+
+
+ ACE Platform API (b3) — API Reference
+
+
+
+
+
AppsCode Cloud Engine (ACE) — b3 API Reference
+ OpenAPI 3.0.3 · generated from the b3 source · 443 paths / 530 operations
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/platform/api/_index.md b/docs/platform/api/_index.md
new file mode 100644
index 0000000..591c3e8
--- /dev/null
+++ b/docs/platform/api/_index.md
@@ -0,0 +1,62 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api
+ name: API Reference
+ weight: 40
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# ACE Platform API Reference
+
+Low-level reference for the AppsCode Cloud Engine (ACE) platform backend (`b3`)
+REST API. Every endpoint lists its HTTP method, path, authentication, path/query
+parameters, and request/response shapes so you can implement a client directly
+against it.
+
+All routes are served under the `/api/v1` prefix unless noted otherwise. The
+marketplace webhook service is a separate listener rooted at `/marketplace/api/v1`.
+
+## Authentication
+
+Most endpoints require a personal access token. Send it as an HTTP header:
+
+```
+Authorization: token
+```
+
+You can also pass it as a `token` or `access_token` query parameter. Token-management
+endpoints accept HTTP Basic auth. The web console uses a session cookie
+(`i_like_ace`); a session cookie alone does **not** authenticate the token-guarded
+REST endpoints — use a token.
+
+A machine-readable [OpenAPI 3.0.3 specification](../openapi.yaml) of this API is also
+available (`openapi.yaml`) — load it into any OpenAPI tool (Swagger UI, Redoc,
+`openapi-generator`) to explore the API or generate a client. A self-contained
+Swagger UI viewer (`api.html`, with the spec inlined) is available at the repository
+root.
+
+## API groups
+
+| Group | What it covers |
+|---|---|
+| [Identity: Users & Settings](../users-settings/) | Accounts, profile/security settings, tokens, credentials |
+| [Identity: Organizations & Teams](../organizations-teams/) | Orgs, members, teams, org tokens |
+| [Administration](../administration/) | Admin console, site settings |
+| [Authorization](../authorization/) | Custom roles & permissions |
+| [Cluster Management v1](../cluster-management-v1/) | Cluster lifecycle, Kubernetes proxy, Helm |
+| [Cluster Management v2](../cluster-management-v2/) | Hub-aware cluster API, subscriptions, gateways |
+| [Multi-cluster (OCM)](../multicluster-ocm/) | Hub/spoke, cluster sets, feature sets |
+| [Client Organizations](../client-organizations/) | Managed-service client orgs |
+| [Cloud Providers](../cloud-providers/) | Provider discovery for provisioning |
+| [ACE Installer](../ace-installer/) | Self-host installer bundles |
+| [ACE Upgrade](../ace-upgrade/) | Platform & cluster upgrades |
+| [Licensing & Contracts](../licensing-contracts/) | Contracts, licenses, registration |
+| [Billing Dashboard](../billing-dashboard/) | Usage reports & billing dashboards |
+| [Marketplace](../marketplace/) | Cloud-marketplace webhooks & metering |
+| [Monitoring & Telemetry](../monitoring-telemetry/) | Telemetry stack, Trickster auth proxy |
+| [Rancher Integration](../rancher/) | Rancher sync & proxy |
+| [Chart Repositories](../chart-repositories/) | Public Helm chart repositories |
+| [Miscellaneous](../miscellaneous/) | Version, markdown, health |
diff --git a/docs/platform/api/ace-installer/_index.md b/docs/platform/api/ace-installer/_index.md
new file mode 100644
index 0000000..0380756
--- /dev/null
+++ b/docs/platform/api/ace-installer/_index.md
@@ -0,0 +1,32 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-ace-installer
+ name: ACE Installer
+ parent: api
+ weight: 100
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# ACE Installer API
+
+The ACE Installer API generates and manages self-host installer bundles for the
+AppsCode Container Engine (ACE) platform. It serves the installer options schema,
+generates and imports installer bundles, lists and inspects generated installers,
+and supports reconfigure/upgrade/versioning and marketplace-installer status.
+
+> **AppsCode-hosted only.** Every route under `/api/v1/ace-installer/...` is available
+> only on the AppsCode-hosted (SaaS) deployment. On self-hosted ACE installations these
+> routes are not registered and return `404 Not Found`. All calls require a bearer token,
+> an org context (resolved from the `org` query param), and per-action authorization
+> checks (`view_installers`, `create_installers`, `import_installers`,
+> `reconfigure_installers`, `upgrade_installers`, `download_installers`,
+> `delete_installers`).
+
+## Pages
+
+- [ACE Installer](../ace-installer.md) — schema/model, generate/import,
+ installer metadata and latest version, installers CRUD, reconfigure/upgrade,
+ versions/archives, and marketplace installer status.
diff --git a/docs/platform/api/ace-installer/ace-installer.md b/docs/platform/api/ace-installer/ace-installer.md
new file mode 100644
index 0000000..85baef9
--- /dev/null
+++ b/docs/platform/api/ace-installer/ace-installer.md
@@ -0,0 +1,457 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-ace-installer-ace-installer
+ name: ACE Installer
+ parent: api-ace-installer
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# ACE Installer
+
+The ACE Installer endpoints generate and manage self-host installer bundles for the
+AppsCode Container Engine (ACE) platform. All paths on this page are relative to the
+API root `/api/v1` — for example `/ace-installer/schema.json` is
+`/api/v1/ace-installer/schema.json`.
+
+> **AppsCode-hosted only.** These routes exist only on the AppsCode-hosted (SaaS)
+> deployment. On self-hosted ACE installations the `/ace-installer` routes are not
+> registered and every call returns `404 Not Found`. (This was confirmed live — see the
+> verification notes below.)
+
+## Authentication and authorization
+
+All endpoints accept a bearer token via any of the supported schemes
+(`Authorization: token `, the `Token` cookie, or an `AccessToken`). Requests are
+scoped to an organization that is resolved from the `org` query parameter. Beyond the
+base token/org check, most endpoints enforce a per-action authorization on the
+organization's hosted-mode capability:
+
+| Action | Required authorization |
+|--------|------------------------|
+| View / list / versions | `view_installers` (`Organization_HostedModeCanViewInstallers`) |
+| Generate | `create_installers` (`Organization_HostedModeCanCreateInstallers`) |
+| Import | `import_installers` (`Organization_HostedModeCanImportInstallers`) |
+| Reconfigure | `reconfigure_installers` (`Organization_HostedModeCanReconfigureInstallers`) |
+| Upgrade | `upgrade_installers` (`Organization_HostedModeCanUpgradeInstallers`) |
+| Download archive | `download_installers` (`Organization_HostedModeCanDownloadInstallers`) |
+| Delete | `delete_installers` (`Organization_HostedModeCanDeleteInstallers`) |
+| Serve installer options (`model.json`) | `create_installers` |
+
+A documented call looks like:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https://appscode.com/api/v1/ace-installer/latest-version?org=appscode"
+```
+
+## Common query parameter
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `org` | string | yes | Organization slug used to resolve the org context (e.g. `appscode`). |
+
+## Data models
+
+### InstallerMetadata
+
+Metadata describing a generated or imported ACE installer.
+
+```json
+{
+ "ID": "inst-01hxyz",
+ "installerName": "acme-prod",
+ "deploymentType": "SelfHosted",
+ "requestedDomain": "ace.example.com",
+ "host": "ace.example.com",
+ "hostType": "Custom",
+ "hostedDomain": "acme.ace.appscode.com",
+ "ownerName": "appscode",
+ "requesterDisplayName": "Example User",
+ "requesterUsername": "user",
+ "adminDisplayName": "Example Admin",
+ "adminEmail": "user@example.com",
+ "accessTokenID": 12345,
+ "clusterID": "",
+ "version": "v2025.7.14",
+ "promotedToProduction": false,
+ "createTimestamp": "2026-07-14T10:00:00Z",
+ "expiryTimestamp": "2026-08-14T10:00:00Z",
+ "lastUpgradeTimestamp": "2026-07-14T10:00:00Z",
+ "lastUpdateTimestamp": "2026-07-14T10:00:00Z"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `ID` | string | Installer ID. |
+| `installerName` | string | Installer name (installers are grouped by name). |
+| `deploymentType` | string | ACE deployment type. |
+| `requestedDomain` | string | Domain requested for the installer. |
+| `host` | string | Effective host for the deployment. |
+| `hostType` | string | Host type. |
+| `hostedDomain` | string | AppsCode-hosted domain assigned to the deployment. |
+| `ownerName` | string | Owner organization slug. |
+| `requesterDisplayName` | string | Display name of the requester. |
+| `requesterUsername` | string | Username of the requester. |
+| `adminDisplayName` | string | Display name of the platform admin. |
+| `adminEmail` | string | Email of the platform admin. |
+| `accessTokenID` | integer (int64) | ID of the access token that created the installer. |
+| `clusterID` | string | Cluster identifier bound to the installer. |
+| `version` | string | ACE installer chart version. |
+| `promotedToProduction` | boolean | Whether the installer has been promoted to production. |
+| `createTimestamp` | string (date-time) | Creation time. |
+| `expiryTimestamp` | string (date-time) | Expiry time. |
+| `lastUpgradeTimestamp` | string (date-time) | Last upgrade time. |
+| `lastUpdateTimestamp` | string (date-time) | Last update time. |
+
+### InstallerDetails
+
+```json
+{
+ "meta": { "...": "InstallerMetadata" },
+ "readme": "",
+ "archiveZipURL": "https://.../installer.zip",
+ "archiveTarURL": "https://.../installer.tar.gz"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `meta` | InstallerMetadata | Installer metadata. |
+| `readme` | string (byte) | Base64-encoded readme content. |
+| `archiveZipURL` | string | Download URL for the `.zip` archive. |
+| `archiveTarURL` | string | Download URL for the `.tar.gz` archive. |
+
+### InstallerVersion
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `version` | string | Version string. |
+| `creationTime` | string (date-time) | When the version/archive was created. |
+| `archiveURL` | string | Archive download URL. |
+| `archiveName` | string | Archive file name. |
+| `createdBy` | User | User who created the version. |
+| `lastOperation` | string | The last operation applied for this version. |
+| `latestInstaller` | boolean | Whether this is the latest installer version. |
+
+### MarketplaceInstallerStatus
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `status` | string | Either `Pending` or `Bound`. |
+| `link` | string | Marketplace / self-host link. |
+| `meta` | InstallerMetadata | Installer metadata. |
+| `isExpired` | boolean | Whether the subscription has expired. |
+
+---
+
+## Schema and default options
+
+### GET /ace-installer/schema.json
+
+Returns the raw OpenAPI v3 JSON schema for ACE installer options
+(`ace-options/values.openapiv3_schema.yaml`) as a dynamic JSON object.
+
+- **Auth:** token; org resolved from the `org` query param.
+- **Query parameters:**
+
+ | Name | Type | Required | Description |
+ |------|------|----------|-------------|
+ | `org` | string | yes | Organization slug. |
+
+- **Response `200`:** an arbitrary Kubernetes-style JSON object (the OpenAPI v3 schema
+ document). The body is free-form and passed through verbatim, so no fixed field set is
+ documented here.
+
+> **Verified:** returned `404` against `appscode` on the self-hosted platform
+> (``) on 2026-07-14 — the ace-installer routes are AppsCode-hosted only and are
+> not registered on this install (body: `{"message":"Not Found"}`).
+
+### GET /ace-installer/model.json
+
+Returns the default ACE options model (`AceOptionsSpec`) pre-filled with the requesting
+user's admin details, as a dynamic JSON object.
+
+- **Auth:** token; org context.
+- **Query parameters:** `org` (string, required).
+- **Response `200`:** an arbitrary Kubernetes-style options JSON object (`AceOptionsSpec`).
+
+> **Verified:** returned `404` against `appscode` on the self-hosted platform on
+> 2026-07-14 (AppsCode-hosted only; route not registered).
+
+---
+
+## Generate and import
+
+### POST /ace-installer/generate
+
+Generates a new ACE installer from the supplied `AceOptionsSpec` options and returns the
+resulting installer metadata.
+
+- **Auth:** token; org context; requires `create_installers`.
+- **Query parameters:** `org` (string, required).
+- **Request body:** a free-form `AceOptionsSpec` object, bound as a dynamic JSON map and
+ then decoded into `AceOptionsSpec`. The set of options mirrors the schema served by
+ `GET /ace-installer/schema.json` (a Kubernetes-style values object); the exact fields are
+ defined by that schema rather than a fixed request model.
+
+ ```json
+ {
+ "...": "AceOptionsSpec (see GET /ace-installer/schema.json for the field set)"
+ }
+ ```
+
+- **Response `200`:** [`InstallerMetadata`](#installermetadata) for the generated installer.
+- **Errors:** `401` unauthorized, `403` missing `create_installers`, `422` invalid body,
+ `500` server error.
+
+> Not verified live (POST — mutating call not issued against the platform).
+
+### POST /ace-installer/import
+
+Imports an existing installer described by the supplied `AceOptionsSpec` options and
+returns the resulting installer metadata.
+
+- **Auth:** token; org context; requires `import_installers`.
+- **Query parameters:** `org` (string, required).
+- **Request body:** a free-form `AceOptionsSpec` object (same shape as `generate`).
+- **Response `200`:** [`InstallerMetadata`](#installermetadata).
+- **Errors:** `401`, `403` missing `import_installers`, `422` invalid body, `500`.
+
+> Not verified live (POST — mutating call not issued against the platform).
+
+---
+
+## Installer metadata and latest version
+
+### GET /ace-installer/installer-meta
+
+Returns the installer metadata associated with the access token used to authenticate the
+request.
+
+- **Auth:** token; org context; requires `view_installers`.
+- **Query parameters:** `org` (string, required).
+- **Response `200`:** [`InstallerMetadata`](#installermetadata).
+
+> **Verified:** returned `404` against `appscode` on the self-hosted platform on
+> 2026-07-14 (AppsCode-hosted only; route not registered).
+
+### GET /ace-installer/latest-version
+
+Returns the latest available ACE installer chart version.
+
+- **Auth:** token; org context; requires `view_installers`.
+- **Query parameters:** `org` (string, required).
+- **Response `200`:**
+
+ ```json
+ { "version": "v2025.7.14" }
+ ```
+
+ | Field | Type | Description |
+ |-------|------|-------------|
+ | `version` | string | Latest ACE installer chart version. |
+
+> **Verified:** returned `404` against `appscode` on the self-hosted platform on
+> 2026-07-14 (AppsCode-hosted only; route not registered).
+
+---
+
+## Installers CRUD
+
+### GET /ace-installer/installers/
+
+Lists the installers for the owner organization, grouped by installer name (one metadata
+entry per group).
+
+- **Auth:** token; org context; requires `view_installers`.
+- **Query parameters:** `org` (string, required).
+- **Response `200`:** an array of [`InstallerMetadata`](#installermetadata).
+
+> **Verified:** returned `404` against `appscode` on the self-hosted platform on
+> 2026-07-14 (AppsCode-hosted only; route not registered).
+
+### GET /ace-installer/installers/{name}
+
+Lists all generated installers matching the given installer name for the owner
+organization.
+
+- **Auth:** token; org context; requires `view_installers`.
+- **Path parameters:**
+
+ | Name | Type | Description |
+ |------|------|-------------|
+ | `name` | string | Installer name. |
+
+- **Query parameters:** `org` (string, required).
+- **Response `200`:** an array of [`InstallerMetadata`](#installermetadata).
+- **Errors:** `401`, `403` missing `view_installers`, `404` not found, `500`.
+
+> Not verified live (requires a real installer name; not present on this self-hosted
+> platform — parent list route returns `404`).
+
+### GET /ace-installer/installers/{name}/{id}
+
+Returns detailed metadata for a single generated installer, including readme content and
+archive download URLs.
+
+- **Auth:** token; org context; requires `view_installers`.
+- **Path parameters:**
+
+ | Name | Type | Description |
+ |------|------|-------------|
+ | `name` | string | Installer name. |
+ | `id` | string | Installer ID. |
+
+- **Query parameters:** `org` (string, required).
+- **Response `200`:** [`InstallerDetails`](#installerdetails).
+- **Errors:** `401`, `403`, `404` installer not found, `428` Precondition Required
+ (a marketplace installer that is not yet in a bound phase returns a redirect to the
+ marketplace URL), `500`.
+
+> Not verified live (requires a real installer name/ID; ace-installer routes not present
+> on this self-hosted platform).
+
+### DELETE /ace-installer/installers/{name}/{id}
+
+Deletes a generated installer by ID. Installers owned by a marketplace organization
+cannot be deleted.
+
+- **Auth:** token; org context; requires `delete_installers`.
+- **Path parameters:** `name` (string), `id` (string).
+- **Query parameters:** `org` (string, required).
+- **Response `200`:** installer deleted (empty body).
+- **Errors:** `401`, `403` missing `delete_installers` or the installer belongs to a
+ marketplace organization, `500`.
+
+> Not verified live (DELETE — mutating call not issued against the platform).
+
+---
+
+## Reconfigure and upgrade
+
+### POST /ace-installer/installers/{name}/{id}/reconfigure
+
+Reconfigures an existing installer using the supplied `AceOptionsSpec` options.
+
+- **Auth:** token; org context; requires `reconfigure_installers`.
+- **Path parameters:** `name` (string), `id` (string).
+- **Query parameters:**
+
+ | Name | Type | Required | Description |
+ |------|------|----------|-------------|
+ | `org` | string | yes | Organization slug. |
+ | `installerOps` | string | yes | Operation to perform — `reconfigure` or `promote-to-prod`. |
+
+- **Request body:** a free-form `AceOptionsSpec` object (same shape as `generate`).
+- **Response `200`:** [`InstallerMetadata`](#installermetadata).
+- **Errors:** `400` invalid `installerOps` or installer mismatch, `401`,
+ `403` missing `reconfigure_installers`, `422` invalid body, `500`.
+
+> Not verified live (POST — mutating call not issued against the platform).
+
+### POST /ace-installer/installers/{name}/{id}/upgrade
+
+Upgrades an existing installer to the latest ACE installer version and returns the updated
+installer metadata.
+
+- **Auth:** token; org context; requires `upgrade_installers`.
+- **Path parameters:** `name` (string), `id` (string).
+- **Query parameters:** `org` (string, required).
+- **Response `200`:** [`InstallerMetadata`](#installermetadata).
+- **Errors:** `400` installer not found, `401`, `403` missing `upgrade_installers`, `500`.
+
+> Not verified live (POST — mutating call not issued against the platform).
+
+---
+
+## Versions and archives
+
+### GET /ace-installer/installers/{name}/{id}/versions
+
+Lists the available versions (archives) for a generated installer.
+
+- **Auth:** token; org context; requires `view_installers`.
+- **Path parameters:** `name` (string), `id` (string).
+- **Query parameters:** `org` (string, required).
+- **Response `200`:** an array of [`InstallerVersion`](#installerversion).
+- **Errors:** `400` installer not found, `401`, `403` missing `view_installers`, `500`.
+
+> Not verified live (requires a real installer name/ID; ace-installer routes not present
+> on this self-hosted platform).
+
+### GET /ace-installer/installers/{name}/{id}/archives/{archiveName}
+
+Extracts and returns installer details (readme, archive URL) from the specified tar
+archive of a generated installer.
+
+- **Auth:** token; org context; requires `download_installers`.
+- **Path parameters:**
+
+ | Name | Type | Description |
+ |------|------|-------------|
+ | `name` | string | Installer name. |
+ | `id` | string | Installer ID. |
+ | `archiveName` | string | Archive file name. |
+
+- **Query parameters:** `org` (string, required).
+- **Response `200`:** [`InstallerDetails`](#installerdetails).
+- **Errors:** `400` installer not found, `401`, `403` missing `download_installers`, `500`.
+
+> Not verified live (requires a real installer name/ID/archive; ace-installer routes not
+> present on this self-hosted platform).
+
+### GET /ace-installer/installers/{name}/{id}/model.json
+
+Returns the stored `AceOptionsSpec` options for a generated installer.
+
+- **Auth:** token; org context; requires `create_installers`.
+- **Path parameters:** `name` (string), `id` (string).
+- **Query parameters:**
+
+ | Name | Type | Required | Description |
+ |------|------|----------|-------------|
+ | `org` | string | yes | Organization slug. |
+ | `configureProductionOptions` | boolean | no | When `true`, adjust options for a self-hosted production deployment. |
+
+- **Response `200`:** an arbitrary Kubernetes-style options JSON object (`AceOptionsSpec`).
+- **Errors:** `401`, `403` missing `create_installers`, `500`.
+
+> Not verified live (requires a real installer name/ID; ace-installer routes not present
+> on this self-hosted platform).
+
+---
+
+## Marketplace installer status
+
+### GET /ace-installer/deployment/marketplace/installers/{installerID}/status
+
+Returns the marketplace subscription status (`Pending` or `Bound`) for a marketplace-type
+installer, along with its metadata, download link, and expiry state. This route does not
+carry an additional installer-specific authorization check beyond the base org token/authz.
+
+- **Auth:** token; org context.
+- **Path parameters:**
+
+ | Name | Type | Description |
+ |------|------|-------------|
+ | `installerID` | string | Installer ID. |
+
+- **Query parameters:**
+
+ | Name | Type | Required | Description |
+ |------|------|----------|-------------|
+ | `org` | string | yes | Organization slug. |
+ | `redirect` | boolean | no | When `true` and the installer is bound, redirect to the self-host URL. |
+
+- **Response `200`:** [`MarketplaceInstallerStatus`](#marketplaceinstallerstatus).
+- **Errors:** `400` installer not found or not a marketplace deployment type, `401`,
+ `403`, `428` Precondition Required (when bound and `redirect=true`, a redirect to the
+ self-host URL is returned), `500`.
+
+> Not verified live (requires a real marketplace installer ID; ace-installer routes not
+> present on this self-hosted platform).
diff --git a/docs/platform/api/ace-upgrade/_index.md b/docs/platform/api/ace-upgrade/_index.md
new file mode 100644
index 0000000..a98b114
--- /dev/null
+++ b/docs/platform/api/ace-upgrade/_index.md
@@ -0,0 +1,46 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-ace-upgrade
+ name: ACE Upgrade
+ parent: api
+ weight: 110
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# ACE Upgrade
+
+APIs for upgrading the ACE platform itself and for upgrading the ACE feature
+stack running inside imported and spoke clusters. Upgrades are FluxCD/Helm-driven
+and run asynchronously; progress and history are tracked in upgrader `ConfigMap`
+data, which these endpoints surface as dynamic key/value maps.
+
+All routes are served under the `/api/v1` prefix. Every endpoint authenticates
+with a personal access token sent as `Authorization: token ` (it may
+also be supplied as a `token` or `access_token` query parameter).
+
+There are two distinct authorization models, which is why the endpoints are split
+across two pages:
+
+- **Platform upgrade** routes (`/api/v1/upgrade*`) act on the ACE platform as a
+ whole. They require an organization context (`?org=`) and **site-admin
+ organization authorization** — `view_upgrade_history:org` for the read routes
+ and `upgrade_platform:org` for the trigger route.
+- **Cluster upgrade** routes (`/api/v1/clusters/{owner}/{cluster}/upgrade*` and
+ `.../spoke/upgrade*`) act on a specific member cluster and are gated by
+ **cluster assignment** for the given owner/cluster (plus a runtime client to
+ that cluster).
+
+## Pages
+
+- [Platform Upgrade](../platform-upgrade.md) — the
+ `/api/v1/upgrade*` endpoints: platform upgrade status, job status, history, and
+ current version, plus triggering a platform upgrade. Requires site-admin org
+ authorization.
+- [Cluster Upgrade](../cluster-upgrade.md) — the
+ `/api/v1/clusters/{owner}/{cluster}/upgrade*` and `.../spoke/upgrade*`
+ endpoints: imported-cluster and spoke-cluster upgrade status, history, current
+ and latest versions, plus triggering cluster/spoke upgrades. Gated by cluster
+ assignment.
diff --git a/docs/platform/api/ace-upgrade/cluster-upgrade.md b/docs/platform/api/ace-upgrade/cluster-upgrade.md
new file mode 100644
index 0000000..7fd0367
--- /dev/null
+++ b/docs/platform/api/ace-upgrade/cluster-upgrade.md
@@ -0,0 +1,269 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-ace-upgrade-cluster-upgrade
+ name: Cluster Upgrade
+ parent: api-ace-upgrade
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Cluster Upgrade
+
+Endpoints for upgrading the ACE feature stack running inside a member cluster.
+There are two flavours:
+
+- **Imported cluster** routes (`/clusters/{owner}/{cluster}/upgrade*`) upgrade the
+ `opscenter-features` Helm release and its managed features in the target
+ cluster.
+- **Spoke cluster** routes (`/clusters/{owner}/{cluster}/spoke/upgrade*`) upgrade
+ a spoke cluster by updating its managed cluster profile binding to the hub's
+ `opscenter-features` version. Spoke status/history are read from the hub
+ cluster's upgrader `ConfigMap`.
+
+Paths below are relative to the API root `/api/v1` (so
+`/clusters/{owner}/{cluster}/upgrade` means
+`https:///api/v1/clusters/{owner}/{cluster}/upgrade`).
+
+**Authentication & authorization.** All routes require a personal access token
+(`Authorization: token `). Access is gated by **cluster assignment**
+for the given `owner`/`cluster` (the server also needs a runtime client to that
+cluster). A caller without an assignment to the cluster receives `403`. Unlike
+the platform upgrade routes, these do **not** use site-admin org authorization and
+do not take an `org` query parameter — the owner is part of the path.
+
+The upgrade status and history responses are dynamic key/value maps read from an
+upgrader `ConfigMap` — the exact keys depend on the cluster's installer and are
+not a fixed schema.
+
+## Path parameters
+
+These apply to every endpoint on this page.
+
+| Name | Type | Description |
+|---|---|---|
+| `owner` | string | Organization slug or username that owns the cluster. |
+| `cluster` | string | Cluster name within the owner scope. |
+
+---
+
+## GET /clusters/{owner}/{cluster}/upgrade
+
+Returns the most recent upgrade status for an imported cluster, read from the
+latest upgrader `ConfigMap` data in that cluster. The response is a dynamic
+key/value map.
+
+- **Auth:** token; cluster assignment for `{owner}/{cluster}`.
+
+**Response — `200 OK`.** A free-form JSON object (arbitrary `ConfigMap` `data`
+keys). Example shape:
+
+```json
+{
+ "status": "completed",
+ "version": "v2026.6.19"
+}
+```
+
+Returns `400` when no upgrade status is recorded in the cluster yet.
+
+> **Verified:** `GET /clusters/appscode/ace/upgrade` returned `400` against
+> `appscode/ace` (hub) ("no upgrade status found") — no feature-stack upgrade has
+> been run on this cluster, so there is no upgrader ConfigMap. On 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/upgrade
+```
+
+---
+
+## GET /clusters/{owner}/{cluster}/upgrade/history
+
+Returns the upgrade history for an imported cluster as a list of `ConfigMap` data
+maps.
+
+- **Auth:** token; cluster assignment for `{owner}/{cluster}`.
+
+**Response — `200 OK`.** A JSON array of dynamic key/value maps. Example:
+
+```json
+[
+ {
+ "status": "completed",
+ "version": "v2026.6.19"
+ }
+]
+```
+
+> **Verified:** `GET /clusters/appscode/ace/upgrade/history` returned `200`
+> against `appscode/ace` (hub) (empty list `[]`) on 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/upgrade/history
+```
+
+---
+
+## GET /clusters/{owner}/{cluster}/upgrade/current-version
+
+Returns the currently installed `opscenter-features` chart version in an imported
+cluster.
+
+- **Auth:** token; cluster assignment for `{owner}/{cluster}`.
+
+**Response — `200 OK`.**
+
+```json
+{
+ "version": "v2026.6.19"
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `version` | string | Currently installed `opscenter-features` chart version in the cluster. |
+
+> **Verified:** `GET /clusters/appscode/ace/upgrade/current-version` returned
+> `200` against `appscode/ace` (hub) (`{"version":"v2026.6.19"}`) on 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/upgrade/current-version
+```
+
+---
+
+## GET /clusters/{owner}/{cluster}/upgrade/latest-version
+
+Returns the latest available `opscenter-features` chart version from the server —
+the target used for imported-cluster upgrades.
+
+- **Auth:** token; cluster assignment for `{owner}/{cluster}`.
+
+**Response — `200 OK`.**
+
+```json
+{
+ "version": "v2026.6.19"
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `version` | string | Latest available chart version the cluster can be upgraded to. |
+
+> **Verified:** `GET /clusters/appscode/ace/upgrade/latest-version` returned `200`
+> against `appscode/ace` (hub) (`{"version":"v2026.6.19"}`) on 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/upgrade/latest-version
+```
+
+---
+
+## POST /clusters/{owner}/{cluster}/upgrade
+
+Triggers an upgrade of the `opscenter-features` Helm release and managed features
+in an imported cluster to the server's current version. Runs asynchronously;
+progress is tracked in an upgrader `ConfigMap` (poll
+`GET /clusters/{owner}/{cluster}/upgrade`).
+
+- **Auth:** token; cluster assignment for `{owner}/{cluster}`.
+- **Request body:** none.
+
+**Response.** `200` when the upgrade has started. Returns `403` when the cluster
+is not assigned to the caller.
+
+```
+curl -X POST -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/upgrade
+```
+
+> Not verified live: this is a mutating (`POST`) endpoint and is documented from
+> the schema only.
+
+---
+
+## GET /clusters/{owner}/{cluster}/spoke/upgrade
+
+Returns the most recent upgrade status for a spoke cluster, read from the hub
+cluster's upgrader `ConfigMap` data for that spoke. The response is a dynamic
+key/value map.
+
+- **Auth:** token; cluster assignment for `{owner}/{cluster}`.
+
+**Response — `200 OK`.** A free-form JSON object (arbitrary `ConfigMap` `data`
+keys). Example shape:
+
+```json
+{
+ "status": "completed",
+ "version": "v2026.6.19"
+}
+```
+
+Returns `400` when no upgrade status is recorded for the spoke yet.
+
+> **Verified:** `GET /clusters/appscode/arnob-dev/spoke/upgrade` returned `400`
+> against `appscode/arnob-dev` (spoke) ("no upgrade status found") — no spoke
+> upgrade recorded on the hub for this cluster. On 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/arnob-dev/spoke/upgrade
+```
+
+---
+
+## GET /clusters/{owner}/{cluster}/spoke/upgrade/history
+
+Returns the upgrade history for a spoke cluster as a list of `ConfigMap` data maps
+(read from the hub cluster).
+
+- **Auth:** token; cluster assignment for `{owner}/{cluster}`.
+
+**Response — `200 OK`.** A JSON array of dynamic key/value maps. Example:
+
+```json
+[
+ {
+ "status": "completed",
+ "version": "v2026.6.19"
+ }
+]
+```
+
+> **Verified:** `GET /clusters/appscode/arnob-dev/spoke/upgrade/history` returned
+> `200` against `appscode/arnob-dev` (spoke) (empty list `[]`) on 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/arnob-dev/spoke/upgrade/history
+```
+
+---
+
+## POST /clusters/{owner}/{cluster}/spoke/upgrade
+
+Triggers an upgrade of a spoke cluster by updating its managed cluster profile
+binding and related features to the hub's `opscenter-features` version. Runs
+asynchronous post-processing.
+
+- **Auth:** token; cluster assignment for `{owner}/{cluster}`.
+- **Request body:** none.
+
+**Response.** `200` when the spoke upgrade has started. Returns `403` when the
+cluster is not assigned to the caller.
+
+```
+curl -X POST -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/arnob-dev/spoke/upgrade
+```
+
+> Not verified live: this is a mutating (`POST`) endpoint and is documented from
+> the schema only.
diff --git a/docs/platform/api/ace-upgrade/platform-upgrade.md b/docs/platform/api/ace-upgrade/platform-upgrade.md
new file mode 100644
index 0000000..042ab0a
--- /dev/null
+++ b/docs/platform/api/ace-upgrade/platform-upgrade.md
@@ -0,0 +1,202 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-ace-upgrade-platform-upgrade
+ name: Platform Upgrade
+ parent: api-ace-upgrade
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Platform Upgrade
+
+Endpoints for upgrading the ACE platform (the `ace-installer` Helm release) as a
+whole and for inspecting its upgrade state. Paths below are relative to the API
+root `/api/v1` (so `/upgrade` means `https:///api/v1/upgrade`).
+
+**Authentication & authorization.** All routes require a personal access token
+(`Authorization: token `). Platform upgrade routes are scoped to an
+organization — pass `?org=` — and require **site-admin organization
+authorization**:
+
+- read routes (`GET`) require `view_upgrade_history:org`
+ (`Organization_SiteAdminCanViewUpgradeHistory`);
+- the trigger route (`POST /upgrade`) requires `upgrade_platform:org`
+ (`Organization_SiteAdminCanUpgradePlatform`).
+
+A request without the required site-admin authorization returns `403`.
+
+The upgrade status and history responses are dynamic key/value maps read from the
+latest upgrader `ConfigMap` — the exact keys depend on the installer and are not
+a fixed schema.
+
+## Query parameters
+
+These apply to every endpoint on this page.
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing the site-admin authorization context. |
+
+---
+
+## GET /upgrade
+
+Returns the most recent ACE platform upgrade status, taken from the latest
+upgrader `ConfigMap` data. The response is a dynamic key/value map.
+
+- **Auth:** token; site-admin org authz `view_upgrade_history:org`.
+- **Query parameters:** `org` (see above).
+
+**Response — `200 OK`.** A free-form JSON object (arbitrary Kubernetes
+`ConfigMap` `data` keys). Example shape:
+
+```json
+{
+ "status": "completed",
+ "version": "v2026.6.19",
+ "startedAt": "2026-06-19T10:02:11Z"
+}
+```
+
+Returns `400` when no upgrade status is recorded yet.
+
+> **Verified:** `GET /upgrade?org=appscode` returned `400` against `appscode`
+> ("no upgrade status found") — no platform upgrade has been run on this
+> environment, so there is no upgrader ConfigMap to report. On 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/upgrade?org=appscode"
+```
+
+---
+
+## GET /upgrade/status
+
+Returns the status of the most recent ACE platform upgrade job — either `pending`
+(with the target `version`) or `completed`.
+
+- **Auth:** token; site-admin org authz `view_upgrade_history:org`.
+- **Query parameters:** `org` (see above).
+
+**Response — `200 OK`.**
+
+```json
+{
+ "status": "completed"
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `status` | string | Job status (`pending` or `completed`). |
+| `version` | string | Target version; present when a job is `pending`. |
+
+> **Verified:** `GET /upgrade/status?org=appscode` returned `200` against
+> `appscode` (`{"status":"completed"}`) on 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/upgrade/status?org=appscode"
+```
+
+---
+
+## GET /upgrade/history
+
+Returns the ACE platform upgrade history as a list of `ConfigMap` data maps, each
+augmented with a `status` field.
+
+- **Auth:** token; site-admin org authz `view_upgrade_history:org`.
+- **Query parameters:** `org` (see above).
+
+**Response — `200 OK`.** A JSON array of dynamic key/value maps (each map's keys
+are `ConfigMap` `data` entries plus a `status`). Example:
+
+```json
+[
+ {
+ "status": "completed",
+ "version": "v2026.6.19",
+ "startedAt": "2026-06-19T10:02:11Z"
+ }
+]
+```
+
+> **Verified:** `GET /upgrade/history?org=appscode` returned `200` against
+> `appscode` (empty list `[]` — no upgrades recorded) on 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/upgrade/history?org=appscode"
+```
+
+---
+
+## GET /upgrade/current-version
+
+Returns the currently installed ACE platform version (the `ace-installer` Helm
+release version).
+
+- **Auth:** token; site-admin org authz `view_upgrade_history:org`.
+- **Query parameters:** `org` (see above).
+
+**Response — `200 OK`.**
+
+```json
+{
+ "version": "v2026.6.19"
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `version` | string | Currently installed platform version. |
+
+Returns `400` when no version can be determined.
+
+> **Verified:** `GET /upgrade/current-version?org=appscode` returned `200`
+> against `appscode` (`{"version":"v2026.6.19"}`) on 2026-07-14.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/upgrade/current-version?org=appscode"
+```
+
+---
+
+## POST /upgrade
+
+Triggers an ACE platform upgrade. The request is a `multipart/form-data` body
+carrying the target version, a Helm values file, and an optional flag to use the
+latest upgrader image. The upgrade runs asynchronously; poll `GET /upgrade/status`
+for progress.
+
+- **Auth:** token; site-admin org authz `upgrade_platform:org`.
+- **Query parameters:** `org` (see above).
+
+**Request body — `multipart/form-data` (`UpgradeOptions`):**
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `version` | string | yes | Target upgrade version. May be overridden by an `installerVersion` field inside the uploaded values file. |
+| `valuesFile` | file (binary) | no | Helm values file (`values.yaml`) applied for the upgrade. |
+| `useLatestImage` | boolean | no | When true, use the latest upgrader image for the target version. |
+
+**Response.** `200` when the upgrade job is accepted and started. Returns `422`
+for an invalid form or values file, and `403` when the caller lacks
+`upgrade_platform:org` authorization.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ -F "version=v2026.6.19" \
+ -F "valuesFile=@values.yaml" \
+ -F "useLatestImage=false" \
+ "https:///api/v1/upgrade?org=appscode"
+```
+
+> Not verified live: this is a mutating (`POST`) endpoint and is documented from
+> the schema only.
diff --git a/docs/platform/api/administration/_index.md b/docs/platform/api/administration/_index.md
new file mode 100644
index 0000000..0acbd3b
--- /dev/null
+++ b/docs/platform/api/administration/_index.md
@@ -0,0 +1,36 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-administration
+ name: Administration
+ parent: api
+ weight: 30
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Administration
+
+The Administration APIs cover two admin surfaces plus the platform's site-settings
+routes. All paths are served under the `/api/v1` prefix.
+
+- **Administrative-org admin** (`/api/v1/admin/*`) — user and organization management
+ performed by admins of the platform's administrative organization.
+- **Site admin console** (`/api/v1/accounts/admin/*`) — the endpoints powering the
+ ACE site-administration UI: usage analytics, users, organizations, clusters, and
+ authentication-source management.
+- **Site settings** (`/allowed-domains`, `/disable-registration`, `/branding`) — a
+ small set of global settings whose read endpoints are **public** and whose write
+ endpoints require site-admin (or org-admin, for branding) authorization.
+
+Almost every write endpoint (and every admin read endpoint) requires **org context**,
+supplied via the `?org=` query parameter, plus a relationship-based
+authorization check. The tables on each page state the exact authorization relation
+required.
+
+## Pages
+
+- [Administrative-Org Admin](../admin-org.md) — `/api/v1/admin/*`: manage users and their organizations.
+- [Site Admin Console](../site-admin-console.md) — `/api/v1/accounts/admin/*`: dashboard, users, orgs, clusters, auth sources.
+- [Site Settings](../site-settings.md) — `/allowed-domains`, `/disable-registration`, `/branding`.
diff --git a/docs/platform/api/administration/admin-org.md b/docs/platform/api/administration/admin-org.md
new file mode 100644
index 0000000..8acb5d3
--- /dev/null
+++ b/docs/platform/api/administration/admin-org.md
@@ -0,0 +1,321 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-administration-admin-org
+ name: Administrative-Org Admin
+ parent: api-administration
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Administrative-Org Admin
+
+User and organization management performed by admins of the platform's
+**administrative organization**. All paths below are relative to `/api/v1`.
+
+**Auth:** every endpoint requires a valid token, an organization supplied via the
+`?org=` query parameter, and the `admin_of_administrative_org` authorization
+relation. Send the token as a header:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/admin/orgs?org=appscode"
+```
+
+All endpoints accept the common **query parameter**:
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `org` | string | yes | Organization slug providing org context (resolved from `?org=`). |
+
+---
+
+## Organizations
+
+### GET /admin/orgs
+
+List all organizations on the platform.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Response:** `200` — an array of `Organization` objects.
+
+```json
+[
+ {
+ "id": 3,
+ "username": "appscode",
+ "full_name": "",
+ "avatar_url": "https:///accounts/avatars?obj=avatars/3-",
+ "description": "",
+ "website": "",
+ "location": "",
+ "rancherManagementClusterEndPoint": "",
+ "visibility": "public",
+ "orgType": 6
+ }
+]
+```
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+---
+
+## Users
+
+### GET /admin/users
+
+List all users on the platform.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Response:** `200` — an array of `User` objects.
+
+```json
+[
+ {
+ "id": 5,
+ "login": "example-user",
+ "username": "example-user",
+ "full_name": "Example User",
+ "email": "user@example.com",
+ "avatar_url": "https:///accounts/avatars?obj=avatars/5-",
+ "is_admin": false,
+ "active": true,
+ "type": 0,
+ "created": "2026-01-01T00:00:00Z",
+ "last_login": "2026-07-01T00:00:00Z"
+ }
+]
+```
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+### POST /admin/users
+
+Create a user.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Request body:** `CreateUserOption`.
+
+```json
+{
+ "username": "example-user",
+ "email": "user@example.com",
+ "password": "",
+ "full_name": "Example User",
+ "login_name": "example-user",
+ "source_id": 0,
+ "must_change_password": true,
+ "send_notify": true
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `username` | string | yes | The new user's username. |
+| `email` | string | yes | Primary email address. |
+| `password` | string | yes | Initial password. |
+| `full_name` | string | no | Display name. |
+| `login_name` | string | no | External login name (for non-local auth sources). |
+| `source_id` | int64 | no | Authentication source ID (0 = built-in). |
+| `must_change_password` | boolean | no | Force a password change on first login. |
+| `send_notify` | boolean | no | Send a registration notification email. |
+
+- **Response:** `201` — the created `User`.
+
+### PATCH /admin/users/{username}
+
+Edit an existing user.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `username` | string | Username of the user to edit. |
+
+- **Request body:** `EditUserOption`.
+
+```json
+{
+ "email": "user@example.com",
+ "full_name": "Example User",
+ "active": true,
+ "admin": false,
+ "prohibit_login": false,
+ "allow_create_organization": true
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `email` | string | yes | Primary email address. |
+| `source_id` | int64 | no | Authentication source ID. |
+| `login_name` | string | no | External login name. |
+| `full_name` | string | no | Display name. |
+| `password` | string | no | New password (if changing). |
+| `must_change_password` | boolean | no | Force a password change on next login. |
+| `website` | string | no | Website URL. |
+| `location` | string | no | Location. |
+| `active` | boolean | no | Whether the account is active. |
+| `admin` | boolean | no | Grant platform-admin. |
+| `allow_git_hook` | boolean | no | Allow git hooks. |
+| `allow_import_local` | boolean | no | Allow importing local repositories. |
+| `prohibit_login` | boolean | no | Prevent the user from signing in. |
+| `allow_create_organization` | boolean | no | Allow the user to create organizations. |
+
+- **Response:** `200` — the updated `User`.
+
+### DELETE /admin/users/{username}
+
+Delete a user.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `username` | string | Username of the user to delete. |
+
+- **Response:** `204` — no content.
+
+### POST /admin/users/{username}/update
+
+Update a user's profile as admin.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `username` | string | Username of the user to update. |
+
+- **Request body:** `Profile`.
+
+```json
+{
+ "name": "example-user",
+ "full_name": "Example User",
+ "email": "user@example.com",
+ "keep_email_private": false,
+ "website": "https://example.com",
+ "location": "Earth",
+ "language": "en-US",
+ "description": "Platform user"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | string | no | Username. |
+| `full_name` | string | no | Display name. |
+| `email` | string | no | Primary email. |
+| `keep_email_private` | boolean | no | Hide the email from public profile. |
+| `website` | string | no | Website URL. |
+| `location` | string | no | Location. |
+| `language` | string | no | Preferred language/locale. |
+| `description` | string | no | Profile description. |
+
+- **Response:** `200` — the updated `Profile`.
+
+### POST /admin/users/{username}/change-password
+
+Change a user's password as admin.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `username` | string | Username of the user. |
+
+- **Request body:** `UpdatePasswordParams`.
+
+```json
+{
+ "old_password": "",
+ "password": "",
+ "retype": ""
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `old_password` | string | no | Current password. |
+| `password` | string | no | New password. |
+| `retype` | string | no | New password, confirmed. |
+
+- **Response:** `200` — password updated.
+
+### GET /admin/users/{username}/orgs
+
+List the organizations a user belongs to.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `username` | string | Username of the user. |
+
+- **Response:** `200` — a list of organizations (the response shape is delegated to
+ the org listing handler; it is returned as an untyped JSON object/array).
+
+> **Verified:** `GET` returned `200` against `appscode` (user `appscode`) on 2026-07-14.
+
+### POST /admin/users/{username}/orgs
+
+Create an organization owned by the given user.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `username` | string | Username that will own the new organization. |
+
+- **Request body:** `CreateOrgOption`.
+
+```json
+{
+ "username": "new-org",
+ "full_name": "New Org",
+ "description": "Example organization",
+ "website": "https://example.com",
+ "location": "Earth",
+ "orgType": "public",
+ "visibility": "public"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `username` | string | yes | Organization slug. |
+| `full_name` | string | no | Display name. |
+| `description` | string | no | Description. |
+| `website` | string | no | Website URL. |
+| `location` | string | no | Location. |
+| `orgType` | string | no | One of `public` (default), `limited`, `private`. |
+| `rancherManagementClusterEndPoint` | string | no | Rancher management cluster endpoint. |
+| `visibility` | string | no | One of ``, `public`, `limited`, `private`. |
+
+- **Response:** `201` — the created `Organization`.
+
+### GET /admin/users/{uid}
+
+Get user information by numeric user ID.
+
+- **Auth:** token + `?org=` + `admin_of_administrative_org`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `uid` | int64 | Numeric user ID. |
+
+> Note: this shares the `/admin/users/{...}` path with the username-based routes
+> above; supply a numeric ID here to look a user up by ID.
+
+- **Response:** `200` — a `User` object.
+
+> **Verified:** returned `404` against `appscode` (uid `1`) on 2026-07-14 — no user with that ID exists; the endpoint is reachable and authorized.
diff --git a/docs/platform/api/administration/site-admin-console.md b/docs/platform/api/administration/site-admin-console.md
new file mode 100644
index 0000000..6b1b0ae
--- /dev/null
+++ b/docs/platform/api/administration/site-admin-console.md
@@ -0,0 +1,486 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-administration-site-admin-console
+ name: Site Admin Console
+ parent: api-administration
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Site Admin Console
+
+The endpoints powering the ACE site-administration UI: usage analytics, users,
+organizations, clusters, and authentication-source management. All paths below are
+relative to `/api/v1`.
+
+**Auth:** every endpoint requires a valid token, an organization supplied via the
+`?org=` query parameter, membership of that organization, and (for most
+endpoints) a specific `:site_admin` authorization relation named per endpoint below.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/accounts/admin/users?org=appscode"
+```
+
+All endpoints accept the common **query parameter**:
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `org` | string | yes | Organization slug providing org context (resolved from `?org=`). |
+
+Several list endpoints return raw internal records (`models.User`,
+`models.Organization`, `models.ClusterInfo`, `models.LoginSource`, ...). In the
+OpenAPI these are represented generically as untyped JSON objects; the examples below
+show the salient fields only.
+
+---
+
+## Dashboard & configuration
+
+### GET /accounts/admin
+
+Site administration usage-analytics dashboard.
+
+- **Auth:** token + `?org=` + membership + `view_usage_analytics:site_admin`.
+- **Response:** `200` — an untyped JSON object with `stats` and `sysStatus` keys.
+
+```json
+{
+ "stats": { "userCount": 12, "orgCount": 5, "clusterCount": 3 },
+ "sysStatus": { "uptime": "5d3h", "goroutines": 210 }
+}
+```
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+### GET /accounts/admin/config
+
+Site administration configuration page. Renders an HTML template rather than a JSON
+body.
+
+- **Auth:** token + `?org=` + membership + site-admin.
+- **Response:** `200` — `text/html`.
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14 (HTML page).
+
+### POST /accounts/admin/config/test_mail
+
+Send a test email, then redirect to the config page.
+
+- **Auth:** token + `?org=` + membership + `send_test_mails:site_admin`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `email` | string | no | Destination address for the test email. |
+
+- **Response:** `302` — redirect to the config page.
+
+### GET /accounts/admin/monitor
+
+Site administration monitor page. Renders an HTML template rather than a JSON body.
+
+- **Auth:** token + `?org=` + membership + site-admin.
+- **Response:** `200` — `text/html`.
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14 (HTML page).
+
+---
+
+## Users
+
+### GET /accounts/admin/users
+
+List all users.
+
+- **Auth:** token + `?org=` + membership + `view_users:site_admin`.
+- **Response:** `200` — an array of raw user records.
+
+```json
+[
+ {
+ "ID": 5,
+ "Name": "example-user",
+ "FullName": "Example User",
+ "Email": "user@example.com",
+ "IsActive": true,
+ "IsAdmin": false
+ }
+]
+```
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+### GET /accounts/admin/users/deleted_accounts
+
+List deactivated user accounts.
+
+- **Auth:** token + `?org=` + membership + `view_deleted_members:site_admin`.
+- **Response:** `200` — an array of raw user records (deactivated).
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+### POST /accounts/admin/users/deleted_accounts/{id}/reactivate
+
+Reactivate a deactivated user.
+
+- **Auth:** token + `?org=` + membership + `reactivate_members:site_admin`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `id` | int64 | Numeric user ID. |
+
+- **Response:** `200` — user reactivated.
+
+### POST /accounts/admin/users/new
+
+Create a new user.
+
+- **Auth:** token + `?org=` + membership + `create_users:site_admin`.
+- **Request body:** `AdminCreateUserPayload`.
+
+```json
+{
+ "loginType": "local",
+ "userName": "example-user",
+ "email": "user@example.com",
+ "fullName": "Example User",
+ "loginName": "example-user",
+ "password": "",
+ "sendNotify": true,
+ "mustChangePassword": true
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `loginType` | string | yes | Authentication source type (e.g. `local`). |
+| `userName` | string | yes | The new user's username. |
+| `email` | string | yes | Primary email address. |
+| `loginName` | string | no | External login name. |
+| `fullName` | string | no | Display name. |
+| `password` | string | no | Initial password. |
+| `sendNotify` | boolean | no | Send a registration notification email. |
+| `mustChangePassword` | boolean | no | Force a password change on first login. |
+
+- **Response:** `201` — user created.
+
+### POST /accounts/admin/users/{userid}
+
+Edit a user.
+
+- **Auth:** token + `?org=` + membership + `update_users:site_admin`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `userid` | int64 | Numeric user ID. |
+
+- **Request body:** `AdminEditUserPayload`.
+
+```json
+{
+ "loginType": "local",
+ "email": "user@example.com",
+ "fullName": "Example User",
+ "active": true,
+ "admin": false,
+ "prohibitLogin": false,
+ "allowCreateOrganization": true,
+ "disableTwoFactor": false
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `loginType` | string | yes | Authentication source type. |
+| `email` | string | yes | Primary email address. |
+| `loginName` | string | no | External login name. |
+| `fullName` | string | no | Display name. |
+| `password` | string | no | New password (if changing). |
+| `website` | string | no | Website URL. |
+| `location` | string | no | Location. |
+| `active` | boolean | no | Whether the account is active. |
+| `admin` | boolean | no | Grant platform-admin. |
+| `allowGitHook` | boolean | no | Allow git hooks. |
+| `allowImportLocal` | boolean | no | Allow importing local repositories. |
+| `allowCreateOrganization` | boolean | no | Allow the user to create organizations. |
+| `prohibitLogin` | boolean | no | Prevent the user from signing in. |
+| `disableTwoFactor` | boolean | no | Disable the user's two-factor authentication. |
+
+- **Response:** `200` — user updated.
+
+### POST /accounts/admin/users/{userid}/delete
+
+Deactivate a user.
+
+- **Auth:** token + `?org=` + membership + `delete_users:site_admin`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `userid` | int64 | Numeric user ID. |
+
+- **Response:** `200` — user deactivated. Returns `400` with a `message` if the user
+ still owns organizations.
+
+### DELETE /accounts/admin/delete/inactive-users
+
+Purge deactivated users that have been inactive for more than 30 days.
+
+- **Auth:** token + `?org=` + membership + `purge_deactivated_users:site_admin`.
+- **Response:** `200` — inactive users purged.
+
+---
+
+## Organizations
+
+### GET /accounts/admin/orgs
+
+List all organizations.
+
+- **Auth:** token + `?org=` + membership + `view_orgs:site_admin`.
+- **Response:** `200` — an array of raw organization records.
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+### GET /accounts/admin/orgs/orphaned/
+
+List orphaned organizations (organizations with no remaining owner).
+
+- **Auth:** token + `?org=` + membership.
+- **Response:** `200` — an array of `Organization` objects.
+
+> **Verified:** returned `400` ("invalid org id") against `appscode` on 2026-07-14 — the handler rejected the request context on this deployment; the route is reachable and authorized.
+
+### DELETE /accounts/admin/orgs/orphaned/delete/{id}
+
+Delete an orphaned organization.
+
+- **Auth:** token + `?org=` + membership + `delete:orphaned-org`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `id` | int64 | Numeric organization ID. |
+
+- **Response:** `200` — organization deleted.
+
+---
+
+## Clusters
+
+### GET /accounts/admin/clusters
+
+List all clusters known to the platform.
+
+- **Auth:** token + `?org=` + membership.
+- **Response:** `200` — an array of raw cluster records (`models.ClusterInfo`).
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+---
+
+## Authentication sources
+
+Manage the platform's login sources (LDAP, PAM, SMTP, OAuth2/OIDC, ...).
+
+### GET /accounts/admin/auths
+
+List authentication sources.
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_authn_source`.
+- **Response:** `200` — an array of raw login-source records.
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+### GET /accounts/admin/auths/auth-types
+
+List the available authentication source types (name/type pairs).
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_authn_source`.
+- **Response:** `200` — an array of type descriptors.
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+### POST /accounts/admin/auths/new
+
+Create an authentication source.
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_authn_source`.
+- **Request body:** `AuthenticationPayload` (fields vary by source type; the common
+ ones are shown below — LDAP, SMTP, and OAuth2/OIDC fields are all part of the same
+ payload).
+
+```json
+{
+ "type": 2,
+ "name": "corporate-ldap",
+ "host": "ldap.example.com",
+ "port": 636,
+ "bindDN": "cn=admin,dc=example,dc=com",
+ "bindPassword": "",
+ "userBase": "ou=users,dc=example,dc=com",
+ "attributeUsername": "uid",
+ "attributeMail": "mail",
+ "isActive": true,
+ "tls": true,
+ "skipVerify": false
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | string | yes | Display name of the source. |
+| `id` | int64 | no | Source ID (on edit). |
+| `type` | integer | no | Numeric source type (LDAP, SMTP, PAM, OAuth2, ...). |
+| `host` | string | no | Server host (LDAP/SMTP). |
+| `port` | integer | no | Server port. |
+| `bindDN` | string | no | LDAP bind DN. |
+| `bindPassword` | string | no | LDAP bind password. |
+| `userBase` | string | no | LDAP user search base. |
+| `userDN` | string | no | LDAP user DN pattern. |
+| `attributeUsername` | string | no | LDAP username attribute. |
+| `attributeName` | string | no | LDAP given-name attribute. |
+| `attributeSurname` | string | no | LDAP surname attribute. |
+| `attributeMail` | string | no | LDAP email attribute. |
+| `attributeSSHPublicKey` | string | no | LDAP SSH public-key attribute. |
+| `attributesInBind` | boolean | no | Fetch attributes during bind. |
+| `usePagedSearch` | boolean | no | Use LDAP paged search. |
+| `searchPageSize` | integer | no | Paged-search page size. |
+| `filter` | string | no | LDAP user filter. |
+| `adminFilter` | string | no | LDAP admin filter. |
+| `isActive` | boolean | no | Whether the source is enabled. |
+| `isSyncEnabled` | boolean | no | Enable periodic user sync. |
+| `smtpAuth` | string | no | SMTP auth mechanism. |
+| `smtpHost` | string | no | SMTP host. |
+| `smtpPort` | integer | no | SMTP port. |
+| `allowedDomains` | string | no | Comma-separated allowed email domains. |
+| `securityProtocol` | integer | no | Security protocol (none/StartTLS/LDAPS). |
+| `tls` | boolean | no | Use TLS. |
+| `skipVerify` | boolean | no | Skip TLS certificate verification. |
+| `pamServiceName` | string | no | PAM service name. |
+| `oauth2Provider` | string | no | OAuth2 provider name. |
+| `oauth2Key` | string | no | OAuth2 client key/ID. |
+| `oauth2Secret` | string | no | OAuth2 client secret. |
+| `openIDConnectAutoDiscoveryURL` | string | no | OIDC auto-discovery URL. |
+| `oauth2UseCustomURL` | boolean | no | Use custom OAuth2 URLs. |
+| `oauth2TokenURL` | string | no | Custom token URL. |
+| `oauth2AuthURL` | string | no | Custom authorize URL. |
+| `oauth2ProfileURL` | string | no | Custom profile URL. |
+| `oauth2EmailURL` | string | no | Custom email URL. |
+
+- **Response:** `201` — authentication source created.
+
+### GET /accounts/admin/auths/{authid}
+
+Get a single authentication source.
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_authn_source`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `authid` | int64 | Numeric authentication source ID. |
+
+- **Response:** `200` — a raw login-source record.
+
+> **Verified:** returned `500` against `appscode` (authid `1`) on 2026-07-14 — no login source with that ID exists on this deployment; the route is reachable and authorized.
+
+### POST /accounts/admin/auths/{authid}
+
+Edit an authentication source.
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_authn_source`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `authid` | int64 | Numeric authentication source ID. |
+
+- **Request body:** `AuthenticationPayload` (same fields as create, above).
+- **Response:** `200` — authentication source updated.
+
+### POST /accounts/admin/auths/{authid}/delete
+
+Delete an authentication source.
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_authn_source`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `authid` | int64 | Numeric authentication source ID. |
+
+- **Response:** `200` — authentication source deleted.
+
+---
+
+## External OAuth2 sources
+
+Manage external OAuth2/social-login providers.
+
+### GET /accounts/admin/external_oauth
+
+List external OAuth2 authentication sources.
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_external_authn_source`.
+- **Response:** `200` — an array of raw external OAuth2 application records.
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+### POST /accounts/admin/external_oauth/new
+
+Create an external OAuth2 authentication source.
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_external_authn_source`.
+- **Request body:** `ExternalOAuth2AppPayload`.
+
+```json
+{
+ "provider": "github",
+ "clientID": "",
+ "clientSecret": "",
+ "callbackURL": "https:///user/oauth2/github/callback"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `provider` | string | no | Provider name (e.g. `github`, `google`). |
+| `clientID` | string | no | OAuth2 client ID. |
+| `clientSecret` | string | no | OAuth2 client secret. |
+| `callbackURL` | string | no | OAuth2 callback/redirect URL. |
+
+- **Response:** `201` — external OAuth2 source created.
+
+### POST /accounts/admin/external_oauth/{provider}
+
+Edit an external OAuth2 authentication source.
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_external_authn_source`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `provider` | string | Provider name to edit. |
+
+- **Request body:** `ExternalOAuth2AppPayload` (same fields as create, above).
+- **Response:** `200` — external OAuth2 source updated.
+
+### POST /accounts/admin/external_oauth/{provider}/delete
+
+Delete an external OAuth2 authentication source.
+
+- **Auth:** token + `?org=` + membership + `site_admin_editor_external_authn_source`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `provider` | string | Provider name to delete. |
+
+- **Response:** `200` — external OAuth2 source deleted.
diff --git a/docs/platform/api/administration/site-settings.md b/docs/platform/api/administration/site-settings.md
new file mode 100644
index 0000000..4f00585
--- /dev/null
+++ b/docs/platform/api/administration/site-settings.md
@@ -0,0 +1,198 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-administration-site-settings
+ name: Site Settings
+ parent: api-administration
+ weight: 30
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Site Settings
+
+Global platform settings: branding, the email-domain whitelist, and the
+new-user-registration toggle. All paths below are relative to `/api/v1`.
+
+**Auth:** each of these settings has a **public** read endpoint (no authentication)
+and a write endpoint gated by a site-admin (or, for branding, org-admin)
+authorization relation. Write endpoints require org context via `?org=`.
+
+```
+curl https:///api/v1/branding
+```
+
+---
+
+## Branding
+
+### GET /branding
+
+Get the current branding configuration (logo, favicons, app name, colors).
+
+- **Auth:** public.
+- **Response:** `200` — a `BrandingResponse` object; `304` if not modified.
+
+```json
+{
+ "logo": "data:image/png;base64,",
+ "favicons": { "16x16": "data:image/png;base64," },
+ "primaryColor": "#326ce5",
+ "runMode": "prod",
+ "deploymentType": "Hosted",
+ "isOfflineInstaller": false,
+ "showAppTag": true,
+ "appName": "ACE"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `logo` | string | Base64-encoded logo image (`data:` URI). |
+| `favicons` | object | Map of favicon sizes to base64-encoded images. |
+| `primaryColor` | string | Primary UI color (hex). |
+| `runMode` | string | Server run mode. |
+| `deploymentType` | string | Deployment type (e.g. `Hosted`). |
+| `isOfflineInstaller` | boolean | Whether this is an offline-installer deployment. |
+| `showAppTag` | boolean | Whether to show the app tag/badge. |
+| `appName` | string | Application display name. |
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14 (logo returned as a base64 `data:image/png` URI).
+
+### POST /branding
+
+Update the branding options.
+
+- **Auth:** token + membership of the organization + `edit_branding_options:org`.
+- **Request body:** `BrandingOptions`.
+
+```json
+{
+ "logo": "data:image/png;base64,",
+ "favicon": "data:image/png;base64,",
+ "primaryColor": "#326ce5",
+ "showAppTag": true,
+ "appName": "ACE"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `logo` | string | no | Base64-encoded logo image. |
+| `favicon` | string | no | Base64-encoded favicon image. |
+| `primaryColor` | string | no | Primary UI color (hex). |
+| `showAppTag` | boolean | no | Whether to show the app tag/badge. |
+| `appName` | string | no | Application display name. |
+
+- **Response:** `200` — branding options updated. `400` for an invalid branding option.
+
+---
+
+## Allowed email domains
+
+The whitelist of email domains permitted to register on the platform.
+
+### GET /allowed-domains
+
+List the configured email-domain whitelist.
+
+- **Auth:** public.
+- **Response:** `200` — an array of domain strings (empty when no whitelist is set).
+
+```json
+["example.com", "acme.io"]
+```
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14 (empty list `[]` — no whitelist configured).
+
+### POST /allowed-domains
+
+Add an email domain to the whitelist.
+
+- **Auth:** token + `?org=` + `edit_allowed_domains:site_admin`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `org` | string | yes | Organization slug providing org context. |
+
+- **Request body:** `DomainOpts`.
+
+```json
+{ "name": "example.com" }
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | string | no | The email domain to add. |
+
+- **Response:** `200` — domain added. `400` for an invalid domain name; `409` if the
+ domain already exists.
+
+### PATCH /allowed-domains
+
+Remove an email domain from the whitelist.
+
+- **Auth:** token + `?org=` + `edit_allowed_domains:site_admin`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `org` | string | yes | Organization slug providing org context. |
+
+- **Request body:** `DomainOpts`.
+
+```json
+{ "name": "example.com" }
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | string | no | The email domain to remove. |
+
+- **Response:** `200` — domain removed. `400` if the domain is not in the whitelist.
+
+---
+
+## User registration
+
+### GET /disable-registration
+
+Get whether new-user registration is disabled.
+
+- **Auth:** public.
+- **Response:** `200` — a `RegistrationOpts` object.
+
+```json
+{ "disableRegistration": false }
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `disableRegistration` | boolean | `true` if new-user registration is disabled. |
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14 (`{"disableRegistration":false}`).
+
+### POST /disable-registration
+
+Enable or disable new-user registration.
+
+- **Auth:** token + `?org=` + `update_user_registration:site_admin`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `org` | string | yes | Organization slug providing org context. |
+
+- **Request body:** `RegistrationOpts`.
+
+```json
+{ "disableRegistration": true }
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `disableRegistration` | boolean | no | Set to `true` to disable new-user registration. |
+
+- **Response:** `200` — registration status updated.
diff --git a/docs/platform/api/authorization/_index.md b/docs/platform/api/authorization/_index.md
new file mode 100644
index 0000000..778da9f
--- /dev/null
+++ b/docs/platform/api/authorization/_index.md
@@ -0,0 +1,31 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-authorization
+ name: Authorization
+ parent: api
+ weight: 40
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Authorization
+
+The Authorization API exposes ACE's custom role management and permission
+inspection, backed by the platform's relationship-based authorization model
+(OpenFGA-style checks). It lets you list the permissions the caller holds on a
+given object, discover the permissions that can be assigned within an
+organization, and create/manage custom roles that bundle permissions and assign
+them to teams.
+
+All endpoints live under the `/api/v1/authz` prefix and require organization
+context. That context is **not** taken from the path — it is supplied as a
+`?org=` query parameter (for example `?org=appscode`). A valid token
+and membership of that organization are required.
+
+## Pages
+
+- [Roles & Permissions](../roles-permissions.md) — object
+ allowed-permissions lookups (single and batch), the available-permissions
+ catalog, role CRUD, and role principal listings.
diff --git a/docs/platform/api/authorization/roles-permissions.md b/docs/platform/api/authorization/roles-permissions.md
new file mode 100644
index 0000000..2d9d09c
--- /dev/null
+++ b/docs/platform/api/authorization/roles-permissions.md
@@ -0,0 +1,433 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-authorization-roles-permissions
+ name: Roles & Permissions
+ parent: api-authorization
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Roles & Permissions
+
+This page documents ACE's custom authorization API: inspecting the permissions
+the caller holds on an object, discovering assignable permissions, and managing
+custom roles.
+
+All paths on this page are relative to the API root `/api/v1`. The full prefix
+for every endpoint here is `/api/v1/authz`.
+
+## Authentication & org context
+
+Every endpoint requires a valid personal access token (or access token). Send it
+as a header:
+
+```
+Authorization: token
+```
+
+In addition, **organization context is mandatory and is supplied via the `?org=`
+query parameter** (not the path). For example, `?org=appscode`. Most endpoints
+also require that the caller be a member of that organization, and several add a
+relationship-based authorization check (`create_role:org`, `view:role`,
+`edit:role`, `delete:role`, `viewer:role`) noted per endpoint.
+
+Error responses use a standard envelope:
+
+```json
+{
+ "message": "human-readable error",
+ "url": "https:///api/swagger"
+}
+```
+
+---
+
+## Object allowed-permissions
+
+These endpoints answer "what may the current requester do to this object?" They
+resolve the caller's relationships to the target object and return the list of
+permission relations they hold.
+
+### Object types
+
+The `objectType` accepted by these endpoints is one of:
+
+| objectType | Refers to |
+|---|---|
+| `user` | A user account (the `objectID` must equal the requester's own user ID) |
+| `organization` | An organization |
+| `team` | A team |
+| `cluster` | A cluster |
+| `contract` | A billing contract |
+| `robot_ac` | A robot account |
+| `token` | An access token |
+
+Any other value yields `400 Bad Request` with an `invalid object type` message.
+
+### GET /authz/objects/{objectType}/{objID}/allowed-permissions
+
+Returns the requester's allowed permissions on a single identified object.
+
+- **Auth:** token (`Authorization: token`, `?token=`, or `?access_token=`) + `?org=` context.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `objectType` | string | The kind of object being inspected. See [Object types](#object-types). |
+| `objID` | integer (int64) | The numeric ID of the object. For `user`, must equal the requester's own user ID. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing org context. |
+
+**Response:** `200 OK` — an `AuthObjectAllowedPermissions` object.
+
+```json
+{
+ "objectType": "team",
+ "objectID": 1,
+ "allowedPermissions": [
+ "can_view",
+ "can_edit",
+ "can_add_member"
+ ]
+}
+```
+
+Fields: `objectType` and `objectID` echo the request; `allowedPermissions` is the
+list of relation strings the requester holds on that object (empty array if none).
+`400` is returned for an invalid object type/ID, `401` if unauthenticated, `403`
+if forbidden.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/authz/objects/team/1/allowed-permissions?org=appscode"
+```
+
+> **Verified:** `GET` returned `200` against `appscode` (org context) on 2026-07-14 for `objectType=team`, `objID=1` (returned `["can_view","can_edit","can_add_member"]`). Note: `objectType=organization` returned `500` in this environment ("Team does not exist"), and an unsupported `objectType` returns `400`.
+
+### POST /authz/objects/allowed-permissions
+
+Batch variant of the above: returns the requester's allowed permissions for each
+supplied object identifier, in the same order.
+
+- **Auth:** token + `?org=` context.
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing org context. |
+
+**Request body:** a JSON array of `AuthObjectIdentifier` objects.
+
+```json
+[
+ { "objectType": "team", "objectID": 1 },
+ { "objectType": "cluster", "objectID": 42 }
+]
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `objectType` | string | yes | The kind of object. See [Object types](#object-types). |
+| `objectID` | integer (int64) | yes | The numeric ID of the object. |
+
+**Response:** `200 OK` — a JSON array of `AuthObjectAllowedPermissions`, one entry
+per requested identifier.
+
+```json
+[
+ {
+ "objectType": "team",
+ "objectID": 1,
+ "allowedPermissions": ["can_view", "can_edit", "can_add_member"]
+ },
+ {
+ "objectType": "cluster",
+ "objectID": 42,
+ "allowedPermissions": ["can_view_kubeconfig"]
+ }
+]
+```
+
+`400` for an invalid object type/ID in any element, `401` unauthenticated, `403`
+forbidden, `422` for a malformed body.
+
+---
+
+## Available permissions
+
+### GET /authz/roles/available_permissions
+
+Returns the catalog of permissions that can be assigned within the caller's
+organization context — the building blocks used when creating or updating a role.
+
+- **Auth:** token + `?org=` context + membership of that organization.
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing org context. |
+
+**Response:** `200 OK`. The body is an `authzstores.VirtualPermissionsData`
+structure (represented generically in the OpenAPI schema). In practice it is a
+`permissions` array where each entry has a `namespace`, an `action`, a
+human-readable `description`, and an optional `associated` list of permissions
+that are implied or required alongside it.
+
+```json
+{
+ "permissions": [
+ {
+ "namespace": "org_mgmt",
+ "action": "edit",
+ "description": "Update organization details and configuration"
+ },
+ {
+ "namespace": "cluster_mgmt",
+ "action": "view clusters",
+ "description": "View clusters associated with the organization and their details",
+ "associated": [
+ { "namespace": "cluster_mgmt", "action": "import clusters", "required": false }
+ ]
+ }
+ ]
+}
+```
+
+The `namespace`/`action` pairs are exactly the values you supply in a role's
+`permissions` list (see below). `401` unauthenticated, `403` forbidden.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/authz/roles/available_permissions?org=appscode"
+```
+
+> **Verified:** `GET` returned `200` against `appscode` (org context) on 2026-07-14; returned a `permissions` catalog spanning namespaces such as `org_mgmt` and `cluster_mgmt`.
+
+---
+
+## Roles
+
+Custom roles bundle a set of permissions and can be attached to one or more teams
+within an organization.
+
+### GET /authz/roles
+
+Lists the custom roles defined in the organization.
+
+- **Auth:** token + `?org=` context + membership of that organization.
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing org context. |
+
+**Response:** `200 OK` — an array of `RoleResponse` objects.
+
+```json
+[
+ {
+ "id": 12,
+ "name": "Cluster Operators",
+ "owner_id": 3,
+ "description": "Can view and reconfigure clusters",
+ "permissions": [
+ { "namespace": "cluster_mgmt", "action": "view clusters" },
+ { "namespace": "cluster_mgmt", "action": "reconfigure clusters" }
+ ],
+ "team_ids": [7],
+ "created_at": "2026-07-01T10:00:00Z",
+ "updated_at": "2026-07-01T10:00:00Z"
+ }
+]
+```
+
+`401` unauthenticated, `403` forbidden. An organization with no custom roles
+returns an empty array `[]`.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/authz/roles?org=appscode"
+```
+
+> **Verified:** `GET` returned `200` against `appscode` (org context) on 2026-07-14; the response was `[]` (no custom roles defined in this org).
+
+### POST /authz/roles
+
+Creates a custom role.
+
+- **Auth:** token + `?org=` context + membership + the `create_role:org` authorization relation.
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing org context. |
+
+**Request body:** a `CreateRoleRequest`.
+
+```json
+{
+ "name": "Cluster Operators",
+ "description": "Can view and reconfigure clusters",
+ "permissions": [
+ { "namespace": "cluster_mgmt", "action": "view clusters" },
+ { "namespace": "cluster_mgmt", "action": "reconfigure clusters" }
+ ],
+ "teamIDs": [7]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `name` | string | yes | Display name of the role. |
+| `permissions` | array of `PermissionInput` | yes | Permissions granted by the role. |
+| `description` | string | no | Human-readable description. |
+| `teamIDs` | array of integer (int64) | no | Teams the role is assigned to. |
+
+Each `PermissionInput` has a `namespace` (a `PermissionNamespace` value) and an
+`action`; use the pairs returned by
+[available_permissions](#get-authzrolesavailablepermissions).
+
+**Response:** `201 Created` — the created `RoleResponse` (same shape as the list
+entry above, with server-assigned `id`, `owner_id`, and timestamps). `400`
+invalid request, `401` unauthenticated, `403` forbidden (missing
+`create_role:org`), `422` validation error.
+
+### GET /authz/roles/{id}
+
+Fetches a single role by ID.
+
+- **Auth:** token + `?org=` context + membership + the `view:role` authorization relation.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `id` | integer (int64) | The role ID. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing org context. |
+
+**Response:** `200 OK` — a `RoleResponse` (see the list example above). `401`
+unauthenticated, `403` forbidden (missing `view:role`), `404` role not found.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/authz/roles/12?org=appscode"
+```
+
+> **Verified:** `GET /authz/roles/1?org=appscode` returned `403` against `appscode` on 2026-07-14 — the caller lacks the `view:role` relation on that role (no matching role exists in this org, so no relation is granted).
+
+### PUT /authz/roles/{id}
+
+Updates a role.
+
+- **Auth:** token + `?org=` context + membership + the `edit:role` authorization relation.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `id` | integer (int64) | The role ID. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing org context. |
+
+**Request body:** an `UpdateRoleRequest` (all fields optional; supplied fields
+replace the current values).
+
+```json
+{
+ "name": "Cluster Operators",
+ "description": "Can view, reconfigure and connect to clusters",
+ "permissions": [
+ { "namespace": "cluster_mgmt", "action": "view clusters" },
+ { "namespace": "cluster_mgmt", "action": "reconfigure clusters" },
+ { "namespace": "cluster_mgmt", "action": "connect to clusters" }
+ ],
+ "teamIDs": [7, 8]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `name` | string | no | New display name. |
+| `description` | string | no | New description. |
+| `permissions` | array of `PermissionInput` | no | Replacement permission set. |
+| `teamIDs` | array of integer (int64) | no | Replacement team assignments. |
+
+**Response:** `200 OK` — the updated `RoleResponse`. `400` invalid request, `401`
+unauthenticated, `403` forbidden (missing `edit:role`), `404` role not found,
+`422` validation error.
+
+### DELETE /authz/roles/{id}
+
+Deletes a role.
+
+- **Auth:** token + `?org=` context + membership + the `delete:role` authorization relation.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `id` | integer (int64) | The role ID. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing org context. |
+
+**Response:** `204 No Content` on success. `401` unauthenticated, `403` forbidden
+(missing `delete:role`), `404` role not found.
+
+---
+
+## Role principals
+
+### GET /authz/roles/{id}/principals
+
+Lists the principals (users/teams) assigned to a role.
+
+- **Auth:** token + `?org=` context + membership + the `viewer:role` authorization relation.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `id` | integer (int64) | The role ID. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug providing org context. |
+
+**Response:** `200 OK` — an array. The body is the raw output of
+`models.ListPrincipalsOfARole` (represented generically in the OpenAPI schema),
+one entry per principal assigned to the role. `401` unauthenticated, `403`
+forbidden (missing `viewer:role`).
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/authz/roles/12/principals?org=appscode"
+```
+
+> **Verified:** `GET /authz/roles/1/principals?org=appscode` returned `403` against `appscode` on 2026-07-14 — the caller lacks the `viewer:role` relation (no matching role exists in this org).
diff --git a/docs/platform/api/billing-dashboard/_index.md b/docs/platform/api/billing-dashboard/_index.md
new file mode 100644
index 0000000..5564fd3
--- /dev/null
+++ b/docs/platform/api/billing-dashboard/_index.md
@@ -0,0 +1,58 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-billing-dashboard
+ name: Billing Dashboard
+ parent: api
+ weight: 130
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Billing Dashboard
+
+APIs that power the license, usage, and billing screens of the ACE web console.
+They report on licensed users and their clusters, per-cluster licenses and licensed
+products, resource event histories, marketplace subscriptions, system-outage
+records, and month-by-month usage summaries (KubeDB, KubeStash, KubeVault, Voyager)
+and DBaaS namespace billing reports.
+
+All routes are served under the `/api/v1` prefix. Every endpoint in this group
+authenticates with a personal access token sent as `Authorization: token `
+(it may also be supplied as a `token` or `access_token` query parameter).
+
+> **Billing-enabled deployments only.** This entire group is registered only when
+> the `b3` backend runs with billing enabled. On a deployment without billing, all
+> of these routes return `404 Not Found` — even with a valid token. The
+> owner-scoped and usage-report routes additionally resolve the acting organization
+> from the `org` query parameter, so include `?org=` on those calls.
+
+## Authorization tiers
+
+Routes in this group fall into three authorization tiers:
+
+- **Site-admin (licensed users):** the `/dashboard/users/*`,
+ `/dashboard/clusters/:cid/licenses/active`, `/dashboard/system-outages/*`, and
+ `/dashboard/marketplaces/*` routes require site-admin authorization
+ (`view_licensed_users`, `view_system_outages`, or `view_marketplace_usage`).
+- **Owner-scoped (`view:contracts`):** the `/dashboard/clusters/*` routes resolve
+ the owner from `?org=` and require the `view:contracts` permission on that
+ organization — this is the "self-service" billing dashboard an org sees for its
+ own clusters.
+- **Usage analytics (site-admin):** the `/dashboard/summary/*` usage-report and
+ download routes require `view_usage_analytics:site_admin`; the `/dbaas/billing/*`
+ reports require org ownership when an org is set.
+
+## Pages
+
+- [Admin Billing Dashboard](../admin-dashboard.md) — the
+ site-admin `/api/v1/dashboard/*` routes: licensed users, their clusters,
+ per-cluster licenses and licensed products, resource/event histories, marketplace
+ subscriptions, and system-outage records.
+- [User Billing Dashboard](../user-dashboard.md) — the
+ owner-scoped `/api/v1/dashboard/clusters/*` routes (`view:contracts`): an
+ organization's own active clusters, licenses, licensed products, and event counts.
+- [Usage Reports](../usage-reports.md) — the
+ `/api/v1/dashboard/summary/*` monthly usage-report views and PDF download, plus
+ the `/api/v1/dbaas/billing/*` namespace billing reports.
diff --git a/docs/platform/api/billing-dashboard/admin-dashboard.md b/docs/platform/api/billing-dashboard/admin-dashboard.md
new file mode 100644
index 0000000..4790228
--- /dev/null
+++ b/docs/platform/api/billing-dashboard/admin-dashboard.md
@@ -0,0 +1,644 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-billing-dashboard-admin-dashboard
+ name: Admin Billing Dashboard
+ parent: api-billing-dashboard
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Admin Billing Dashboard
+
+Site-admin endpoints under `/api/v1/dashboard` that back the platform-wide license
+and billing administration screens: licensed users and their clusters, per-cluster
+licenses and licensed products, resource/event histories, marketplace
+subscriptions, and system-outage records.
+
+All paths on this page are relative to `/api/v1`. Every endpoint requires
+`Authorization: token `, resolves the acting organization from the
+`org` query parameter (`?org=`), and requires site-admin authorization on
+that org. This whole group is available only on **billing-enabled deployments**.
+
+> **Verified:** every endpoint on this page returned `404 Not Found` against
+> `appscode` on `` on 2026-07-14 — this deployment is not billing-enabled,
+> so none of the `/dashboard/*` routes are registered. (Sanity: `GET /version` and
+> `GET /user` returned `200` with the same token, confirming the platform and token
+> are live.) Response bodies were the standard `{"message":"Not Found"}`.
+
+Shared conventions on this page:
+
+- **`uid`** is the numeric user (account) ID; **`cid`** is a cluster UID string;
+ **`lid`** is a license ID string.
+- **`limit`** (query, optional) sets the "active" window: a Go duration such as
+ `720h` or a `YYYY-MM-DD` date. When omitted it defaults to roughly the last six
+ months.
+- Timestamps in these forms are Unix epoch values (`int64`) unless noted.
+
+## Licensed users
+
+### GET /dashboard/users/
+
+Lists all users that have ever reported a license to this deployment.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug used to resolve site-admin context. |
+
+- **Response:** `200 OK` — an array of licensed users.
+
+```json
+[
+ {
+ "username": "alice",
+ "userType": "user",
+ "email": "user@example.com",
+ "userID": 42,
+ "lastReceivedAt": 1718000000
+ }
+]
+```
+
+`403` is returned when the caller is not a site admin; `401` when unauthenticated.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/dashboard/users/?org=appscode"
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled on this deployment.
+
+### GET /dashboard/users/active
+
+Lists licensed users that have reported within the `limit` window (defaults to the
+last ~6 months).
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug. |
+| `limit` | string | no | Activity window: Go duration (e.g. `720h`) or `YYYY-MM-DD` date. |
+
+- **Response:** `200 OK` — an array of `LicensedUserAPIForm` (same shape as above).
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/inactive
+
+Lists licensed users that have never reported a licensed product.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Response:** `200 OK` — an array of `LicensedUserAPIForm`.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## A user's clusters
+
+### GET /dashboard/users/{uid}/clusters/
+
+Lists cluster information for the given licensed user.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `uid` | integer | User (account) ID. |
+
+- **Response:** `200 OK` — an array of `ClusterInfo` objects.
+
+```json
+[
+ {
+ "id": 101,
+ "displayName": "ACE Hub",
+ "name": "ace",
+ "uid": "",
+ "ownerID": 42,
+ "ownerName": "appscode",
+ "provider": "GKE",
+ "kubernetesVersion": "v1.29.4",
+ "nodeCount": 3,
+ "createdAt": "2026-01-10T09:00:00Z",
+ "status": { "...": "rsapi.ClusterStatus (free-form Kubernetes-style object)" }
+ }
+]
+```
+
+The `status` field is a free-form Kubernetes-style object passed through verbatim.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/{uid}/clusters/active
+
+Lists clusters that reported for the given user within the `limit` window.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer) — user ID.
+- **Query parameters:** `limit` (string, optional) — activity window.
+- **Response:** `200 OK` — an array of `ActiveClustersAPIForm`.
+
+```json
+[
+ {
+ "username": "alice",
+ "clusterID": "",
+ "firstReceivedAt": 1700000000,
+ "lastReceivedAt": 1718000000,
+ "noOfAssociatedContracts": 2
+ }
+]
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/{uid}/clusters/{cid}
+
+Returns cluster information for a specific user/cluster.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `uid` | integer | User (account) ID. |
+| `cid` | string | Cluster UID. |
+
+- **Response:** `200 OK` — a single `ClusterInfo` object (see above). `404` if the
+ user/cluster pair is unknown.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## A user's licenses on a cluster
+
+### GET /dashboard/users/{uid}/clusters/{cid}/licenses/
+
+Lists licensed plans associated with the given user and cluster.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer), `cid` (string).
+- **Response:** `200 OK` — an array of `LicensedPlan`.
+
+```json
+[
+ {
+ "uid": 42,
+ "email": "user@example.com",
+ "licenseID": "",
+ "clusters": [""],
+ "planName": "kubedb-enterprise",
+ "notBefore": 1700000000,
+ "notAfter": 1731600000,
+ "registeredAt": 1700000000,
+ "createdAt": 1700000000,
+ "updatedAt": 1718000000
+ }
+]
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/{uid}/clusters/{cid}/licenses/active
+
+Lists licenses that reported within the `limit` window for the given user and cluster.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer), `cid` (string).
+- **Query parameters:** `limit` (string, optional) — activity window.
+- **Response:** `200 OK` — an array of `ActiveLicenseAPIForm`.
+
+```json
+[
+ {
+ "username": "alice",
+ "email": "user@example.com",
+ "userId": 42,
+ "licenseID": "",
+ "firstReceivedAt": 1700000000,
+ "lastReceivedAt": 1718000000
+ }
+]
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/{uid}/clusters/{cid}/licenses/{lid}
+
+Returns a licensed plan together with the latest reported `SiteInfo`.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer), `cid` (string), `lid` (string) — license ID.
+- **Response:** `200 OK` — an object with a `license` (a `LicensedPlan`, see above)
+ and a `siteinfo` (a kmodules identity `SiteInfo`, a free-form Kubernetes-style
+ object).
+
+```json
+{
+ "license": { "licenseID": "", "planName": "kubedb-enterprise", "clusters": [""] },
+ "siteinfo": { "...": "kmodules SiteInfo (free-form Kubernetes object)" }
+}
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## Licensed products for a license
+
+### GET /dashboard/users/{uid}/clusters/{cid}/licenses/{lid}/products/
+
+Lists reported licensed products for the given user/cluster/license.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer), `cid` (string), `lid` (string).
+- **Query parameters:** `limit` (string, optional) — activity window.
+- **Response:** `200 OK` — an array of `ReceivedLicensedProduct`.
+
+```json
+[
+ {
+ "userName": "alice",
+ "userType": "user",
+ "email": "user@example.com",
+ "userID": 42,
+ "clusterID": "",
+ "licenseID": "",
+ "productName": "kube-db",
+ "firstReceivedAt": 1700000000,
+ "lastReceivedAt": 1718000000
+ }
+]
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/{uid}/clusters/{cid}/licenses/{lid}/products/{productName}
+
+Returns the reported licensed product for a specific product.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer), `cid` (string), `lid` (string),
+ `productName` (string).
+- **Query parameters:** `limit` (string, optional) — activity window.
+- **Response:** `200 OK` — a single `ReceivedLicensedProduct` (see above).
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/{uid}/clusters/{cid}/licenses/{lid}/products/{productName}/resources
+
+Fetches the badger resource history for a product from the member cluster over NATS.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer), `cid` (string), `lid` (string),
+ `productName` (string).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `group` | string | no | Kubernetes API group filter. |
+| `resource` | string | no | Kubernetes resource filter. |
+
+- **Response:** `200 OK` — a `BadgerEntryList` of resource-history entries. Each
+ entry carries a `key`, a `lastSeen` timestamp, and a list of `BadgerValue`
+ versions; the `resource`/`resourceID` inside each value are free-form Kubernetes
+ objects passed through from the member cluster.
+
+```json
+{
+ "items": [
+ {
+ "key": "",
+ "lastSeen": 1718000000,
+ "values": [
+ {
+ "resourceID": { "...": "free-form Kubernetes object" },
+ "licenseID": "",
+ "version": 3,
+ "timestamp": 1718000000
+ }
+ ]
+ }
+ ]
+}
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/{uid}/clusters/{cid}/licenses/{lid}/products/{productName}/events-histories
+
+Returns an events-history report for a specific resource (admin/testing use).
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer), `cid` (string), `lid` (string),
+ `productName` (string).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `group` | string | no | Kubernetes API group. |
+| `resource` | string | no | Kubernetes resource. |
+| `rid` | string | no | Resource ID (object UID). |
+
+- **Response:** `200 OK` — an `EventsReport`.
+
+```json
+{
+ "userID": 42,
+ "clusterID": "",
+ "licenseID": "",
+ "group": "kubedb.com",
+ "resource": "mongodbs",
+ "resourceID": "",
+ "totalEvents": 128,
+ "eventsTimeHistories": ["2026-06-01T00:00:00Z", "2026-06-02T00:00:00Z"],
+ "eventsDurationHours": 720.0,
+ "averageEventsPerHour": 0.18
+}
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/{uid}/clusters/{cid}/licenses/{lid}/products/{productName}/events/
+
+Returns the tabular event list for a licensed user's cluster/license/product.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer), `cid` (string), `lid` (string),
+ `productName` (string).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `lastDays` | integer | no | Look-back window in days (max 14; defaults to 14). |
+
+- **Response:** `200 OK` — an array of `ClusterResourceTabularForm`, newest first.
+
+```json
+[
+ {
+ "key": "",
+ "version": 3,
+ "timestamp": 1718000000,
+ "partialResource": {
+ "partialObjectMeta": { "...": "free-form partial Kubernetes object metadata" }
+ }
+ }
+]
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/users/{uid}/clusters/{cid}/licenses/{lid}/products/{productName}/events/raw-event
+
+Returns the raw badger value for a specific event key/version.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `uid` (integer), `cid` (string), `lid` (string),
+ `productName` (string).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `key` | string | yes | Badger event key. |
+| `version` | integer | yes | Event version (uint). |
+
+- **Response:** `200 OK` — a single `BadgerValue` (see the resources endpoint
+ above). `400` when `key`/`version` are missing or invalid.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## Active licenses for a cluster
+
+### GET /dashboard/clusters/{cid}/licenses/active
+
+Lists licenses that reported within the `limit` window for the given cluster, across
+all users.
+
+- **Auth:** token; site-admin (`view_licensed_users:site_admin`). Requires `?org=`.
+- **Path parameters:** `cid` (string) — cluster UID.
+- **Query parameters:** `limit` (string, optional) — activity window.
+- **Response:** `200 OK` — an array of `ActiveLicenseAPIForm` (see above).
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## System outages
+
+### POST /dashboard/system-outages/
+
+Records a system-outage window for an account/cluster.
+
+- **Auth:** token; site-admin (`view_system_outages:site_admin`). Requires `?org=`.
+- **Request body:** a `SystemOutageRequest`.
+
+```json
+{
+ "accountName": "appscode",
+ "clusterID": "",
+ "start": "2026-06-01T00:00:00Z",
+ "end": "2026-06-01T02:30:00Z",
+ "tags": ["network", "planned"],
+ "comments": "Planned maintenance window."
+}
+```
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `accountName` | string | no | Account (organization/user) name the outage applies to. |
+| `clusterID` | string | no | Cluster UID the outage applies to. |
+| `start` | date-time | yes | Outage start (RFC3339). |
+| `end` | date-time | yes | Outage end (RFC3339). |
+| `tags` | string[] | no | Free-form tags for grouping/reporting. |
+| `comments` | string | no | Human-readable notes. |
+
+- **Response:** `201 Created` — the stored `SystemOutageApiForm` (fields as above
+ plus a server-assigned `id`, `accountID`, and `createdAt`). `409` if an
+ overlapping entry already exists; `422` on validation failure.
+
+### GET /dashboard/system-outages/report
+
+Lists system-outage entries with an aggregated report (top tags, accounts, and
+clusters by duration).
+
+- **Auth:** token; site-admin (`view_system_outages:site_admin`). Requires `?org=`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `start` | string | no | RFC3339 start-time filter. |
+| `account_id` | string | no | Filter by account ID. |
+| `cluster_id` | string | no | Filter by cluster ID. |
+| `tags` | string[] | no | Filter by tags (repeatable). |
+
+- **Response:** `200 OK` — a `SystemOutagesAndReportAPIForm` with an aggregated
+ `report` and the matching `entries`.
+
+```json
+{
+ "report": {
+ "totalOutages": 3,
+ "totalDurationHours": 6.5,
+ "topTagsPercentage": [{ "tag": "network", "percentage": 66.7 }],
+ "topAccountsDuration": [
+ { "entity": "account", "entityName": "appscode", "durationHours": 4.0, "percentage": 61.5 }
+ ],
+ "topClustersDuration": [
+ { "entity": "cluster", "entityName": "ace", "durationHours": 2.5, "percentage": 38.5 }
+ ]
+ },
+ "entries": [
+ {
+ "id": 1,
+ "accountID": 42,
+ "accountName": "appscode",
+ "clusterID": "",
+ "start": "2026-06-01T00:00:00Z",
+ "end": "2026-06-01T02:30:00Z",
+ "tags": ["network"],
+ "comments": "Planned maintenance window.",
+ "createdAt": "2026-06-01T03:00:00Z"
+ }
+ ]
+}
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/system-outages/tags
+
+Lists all unique tags used across system-outage entries.
+
+- **Auth:** token; site-admin (`view_system_outages:site_admin`). Requires `?org=`.
+- **Response:** `200 OK` — an array of tag strings.
+
+```json
+["network", "planned", "storage"]
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## Marketplace subscriptions
+
+### GET /dashboard/marketplaces/subscriptions
+
+Lists all marketplace subscriptions (minimal API form), newest-updated first.
+
+- **Auth:** token; site-admin (`view_marketplace_usage:site_admin`). Requires `?org=`.
+- **Response:** `200 OK` — an array of `Subscription`.
+
+```json
+[
+ {
+ "marketplace": "Azure",
+ "subscriptionId": "",
+ "subscriberId": 42,
+ "requesterId": 42,
+ "boundedInstallerId": "",
+ "state": "Running",
+ "isRevocable": true,
+ "noOfBillingEventSent": 12,
+ "createdAt": "2026-01-10T09:00:00Z",
+ "lastUpdatedAt": "2026-06-01T00:00:00Z"
+ }
+]
+```
+
+`marketplace` is one of `Aws`, `Azure`, `Gcp`; `state` is one of `Pending`,
+`Bounded`, `Running`, `Failed`, `Deleting`, `DeletionFailed`, `Deleted`, `Revoked`.
+The per-marketplace `awsBillingDetails` / `azureBillingDetails` / `gcpBillingDetails`
+objects are omitted here for brevity (they carry marketplace-specific billing
+identifiers).
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/marketplaces/settings/warnings
+
+Returns configuration warnings for marketplace integrations (e.g. Azure
+client-secret expiry).
+
+- **Auth:** token; site-admin (`view_marketplace_usage:site_admin`). Requires `?org=`.
+- **Response:** `200 OK` — a `SettingWarnings` object: a `warnings` map keyed by
+ marketplace, each value a list of `{ "Level": "...", "Msg": "..." }` warnings.
+
+```json
+{
+ "warnings": {
+ "Azure": [{ "Level": "warning", "Msg": "Client secret expires in 14 days." }]
+ }
+}
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/marketplaces/{marketplace}/{subscriptionId}
+
+Returns the full API form of a marketplace subscription.
+
+- **Auth:** token; site-admin (`view_marketplace_usage:site_admin`). Requires `?org=`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `marketplace` | string | Marketplace identifier (`Aws`, `Azure`, `Gcp`). |
+| `subscriptionId` | string | Marketplace subscription ID. |
+
+- **Response:** `200 OK` — a `Subscription` (see above). `400` if the subscription
+ is not found or invalid.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### DELETE /dashboard/marketplaces/{marketplace}/{subscriptionId}
+
+Revokes a revocable marketplace subscription and all associated contracts.
+
+- **Auth:** token; site-admin (`view_marketplace_usage:site_admin`). Requires `?org=`.
+- **Path parameters:** `marketplace` (string), `subscriptionId` (string).
+- **Response:** `200 OK` on success. `400` if the subscription is not found or not
+ revocable.
+
+### GET /dashboard/marketplaces/{marketplace}/{subscriptionId}/ping
+
+Pings the hosted `b3` metering-readiness endpoint for a subscription (AWS or GCP
+only).
+
+- **Auth:** token; site-admin (`view_marketplace_usage:site_admin`). Requires `?org=`.
+- **Path parameters:** `marketplace` (string), `subscriptionId` (string).
+- **Response:** `200 OK` when the endpoint is reachable. `400` if the subscription
+ is not found or the marketplace is unsupported.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/marketplaces/{marketplace}/{subscriptionId}/audit-logs
+
+Returns audit-event reports for a marketplace subscription, grouped by audit period.
+
+- **Auth:** token; site-admin (`view_marketplace_usage:site_admin`). Requires `?org=`.
+- **Path parameters:** `marketplace` (string), `subscriptionId` (string).
+- **Response:** `200 OK` — an `AuditReportsResp`: an `auditReports` map keyed by
+ audit period, each value a list of `MarketplaceAuditEvent`.
+
+```json
+{
+ "auditReports": {
+ "2026-06": [
+ {
+ "marketplace": "Azure",
+ "subscriptionID": "",
+ "status": "success",
+ "operationType": "report-usage",
+ "description": "Reported metered usage.",
+ "data": { "...": "free-form event payload" },
+ "timestamp": "2026-06-01T00:00:00Z"
+ }
+ ]
+ }
+}
+```
+
+`400` if the subscription is not found.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
diff --git a/docs/platform/api/billing-dashboard/usage-reports.md b/docs/platform/api/billing-dashboard/usage-reports.md
new file mode 100644
index 0000000..aae06d9
--- /dev/null
+++ b/docs/platform/api/billing-dashboard/usage-reports.md
@@ -0,0 +1,309 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-billing-dashboard-usage-reports
+ name: Usage Reports
+ parent: api-billing-dashboard
+ weight: 30
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Usage Reports
+
+Endpoints that serve the pre-generated monthly usage summaries under
+`/api/v1/dashboard/summary` (KubeDB, KubeStash, KubeVault, Voyager) and the DBaaS
+namespace billing reports under `/api/v1/dbaas/billing`.
+
+All paths on this page are relative to `/api/v1`. Every endpoint requires
+`Authorization: token ` and resolves the acting organization from the
+`org` query parameter (`?org=`). The `/dashboard/summary/*` routes require
+`view_usage_analytics:site_admin`; the `/dbaas/billing/*` routes require org
+ownership when an org is set. This group is available only on **billing-enabled
+deployments**.
+
+> **Verified:** every endpoint on this page returned `404 Not Found` against
+> `appscode` on `` on 2026-07-14 — this deployment is not billing-enabled,
+> so neither the `/dashboard/summary/*` nor the `/dbaas/billing/*` routes are
+> registered. (Sanity: `GET /version` and `GET /user` returned `200` with the same
+> token.)
+
+## About usage-report views
+
+Usage summaries are generated per month and per product. The individual view
+endpoints (`objects`, `clusters`, `namespaces`, `gks`, `contracts`, quota-history,
+cluster-mode) all return a **`UsageView`** — a complex, deeply-nested aggregation
+structure whose exact shape varies by product and by the `view_type` query
+parameter. It is modeled as a free-form JSON object, so the examples below show the
+envelope rather than an exhaustive field list.
+
+Common path parameters for the `summary` routes:
+
+| Name | Type | Description |
+|---|---|---|
+| `year` | string | Four-digit report year (e.g. `2026`). |
+| `month` | string | Report month (e.g. `06` or `June`, per the generated-months list). |
+
+## Generated months
+
+### GET /dashboard/summary/generated-months
+
+Returns the year/month combinations for which a usage summary has been generated for
+the owner.
+
+- **Auth:** token; site-admin (`view_usage_analytics:site_admin`). Requires `?org=`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug. |
+
+- **Response:** `200 OK` — a `GeneratedDates` object. `yearMonthList` is keyed by
+ year, with a list of generated month names per year.
+
+```json
+{
+ "yearMonthList": {
+ "2026": ["January", "February", "March", "April", "May", "June"]
+ }
+}
+```
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/dashboard/summary/generated-months?org=appscode"
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled on this deployment.
+
+### GET /dashboard/summary/object-quota-history/clusters/{clusterUID}/objects/{objectID}
+
+Returns the quota-history usage view for a specific KubeDB object (not scoped to a
+month).
+
+- **Auth:** token; site-admin (`view_usage_analytics:site_admin`). Requires `?org=`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `clusterUID` | string | Cluster UID. |
+| `objectID` | string | Object (database) ID. |
+
+- **Response:** `200 OK` — a `UsageView` (free-form object quota-history view).
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## KubeDB usage views
+
+All routes below are rooted at
+`/dashboard/summary/{year}/{month}/usage-report/products/kubeDb/views` and require
+`view_usage_analytics:site_admin` and `?org=`. Each returns a `UsageView`.
+
+### GET .../kubeDb/views/objects-usage-view
+
+Returns the KubeDB objects usage view for the month, enriched with each object's
+daily event count.
+
+- **Query parameters (all optional):** `view_type` (string), `group` (string),
+ `kind` (string), `clusterUID` (string), `namespace` (string), `namespaceUID`
+ (string).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+```json
+{
+ "...": "free-form KubeDB objects usage view (varies by view_type)"
+}
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeDb/views/clusters-usage-view
+
+Returns the KubeDB clusters usage view for the month.
+
+- **Query parameters (all optional):** `view_type` (string), `group` (string),
+ `kind` (string), `contractID` (integer).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeDb/views/namespaces-usage-view
+
+Returns the KubeDB namespaces usage view for the month.
+
+- **Query parameters (all optional):** `view_type` (string), `clusterUID` (string).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeDb/views/gks-usage-view
+
+Returns the KubeDB "GKs" (group/kind) usage view for the month, with the total
+monthly usage matrix.
+
+- **Query parameters (all optional):** `view_type` (string), `clusterUID` (string).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeDb/views/contracts-usage-view
+
+Returns the KubeDB contracts usage view for the month.
+
+- **Query parameters (all optional):** `view_type` (string).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeDb/views/object-quota-history-summaries-view
+
+Returns the month-specific KubeDB object quota-history summaries.
+
+- **Response:** `200 OK` — a `UsageView`.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeDb/views/object-quota-history-summaries-view/clusters/{clusterUID}/objects/{objectID}
+
+Returns the month-specific KubeDB object quota-history summary for a specific
+cluster/object.
+
+- **Path parameters:** `clusterUID` (string), `objectID` (string).
+- **Response:** `200 OK` — a `UsageView`.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeDb/views/objects-cluster-mode-usage-view
+
+Returns the KubeDB objects cluster-mode usage history list for the month.
+
+- **Response:** `200 OK` — a `UsageView`.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeDb/views/objects-cluster-mode-usage-view/clusters/{clusterUID}/objects/{objectID}
+
+Returns the KubeDB cluster-mode usage history for a specific cluster/object.
+
+- **Path parameters:** `clusterUID` (string), `objectID` (string).
+- **Response:** `200 OK` — a `UsageView`.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## KubeStash, KubeVault, and Voyager usage views
+
+Each of these products exposes a `clusters-usage-view` and a `contracts-usage-view`
+for the month, rooted at
+`/dashboard/summary/{year}/{month}/usage-report/products/{product}/views`. They
+require `view_usage_analytics:site_admin` and `?org=`, and each returns a
+`UsageView`.
+
+### GET .../kubeStash/views/clusters-usage-view
+
+Returns the KubeStash clusters usage view for the month.
+
+- **Query parameters (all optional):** `view_type` (string), `contractID` (integer).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeStash/views/contracts-usage-view
+
+Returns the KubeStash contracts usage view for the month.
+
+- **Query parameters (all optional):** `view_type` (string).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeVault/views/clusters-usage-view
+
+Returns the KubeVault clusters usage view for the month.
+
+- **Query parameters (all optional):** `view_type` (string), `contractID` (integer).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../kubeVault/views/contracts-usage-view
+
+Returns the KubeVault contracts usage view for the month.
+
+- **Query parameters (all optional):** `view_type` (string).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../voyager/views/clusters-usage-view
+
+Returns the Voyager clusters usage view for the month.
+
+- **Query parameters (all optional):** `view_type` (string), `contractID` (integer).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET .../voyager/views/contracts-usage-view
+
+Returns the Voyager contracts usage view for the month.
+
+- **Query parameters (all optional):** `view_type` (string).
+- **Response:** `200 OK` — a `UsageView`. `400` on invalid parameters.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## Download
+
+### GET /dashboard/summary/{year}/{month}/download
+
+Renders and returns the KubeDB usage summary as a PDF document for the given month.
+
+- **Auth:** token; site-admin (`view_usage_analytics:site_admin`). Requires `?org=`.
+- **Path parameters:** `year` (string), `month` (string).
+- **Response:** `200 OK` — the usage-report PDF (`application/pdf`, or
+ `application/octet-stream`) as a binary body.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/dashboard/summary/2026/June/download?org=appscode" \
+ -o usage-report.pdf
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## DBaaS billing reports
+
+These routes are rooted at `/dbaas/billing/reports`, require a token and `?org=`,
+and are checked for org ownership when an org is set (rather than site-admin). Each
+returns a pre-marshaled report view, modeled as a free-form `UsageView`.
+
+### GET /dbaas/billing/reports/namespaces
+
+Returns the DBaaS namespace report list (V1) for the owner.
+
+- **Auth:** token; org-ownership when `org` is set. Requires `?org=`.
+- **Response:** `200 OK` — a `UsageView` (free-form namespace report list).
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/dbaas/billing/reports/namespaces?org=appscode"
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled on this deployment.
+
+### GET /dbaas/billing/reports/clusters/{clusterID}/namespaces/{namespaceName}
+
+Returns the DBaaS namespace report (V1) for a specific cluster/namespace.
+
+- **Auth:** token; org-ownership when `org` is set. Requires `?org=`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `clusterID` | string | Cluster ID. |
+| `namespaceName` | string | Namespace name. |
+
+- **Response:** `200 OK` — a `UsageView` (free-form namespace report).
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
diff --git a/docs/platform/api/billing-dashboard/user-dashboard.md b/docs/platform/api/billing-dashboard/user-dashboard.md
new file mode 100644
index 0000000..7a66c43
--- /dev/null
+++ b/docs/platform/api/billing-dashboard/user-dashboard.md
@@ -0,0 +1,269 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-billing-dashboard-user-dashboard
+ name: User Billing Dashboard
+ parent: api-billing-dashboard
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# User Billing Dashboard
+
+Owner-scoped endpoints under `/api/v1/dashboard/clusters` that back the self-service
+billing dashboard an organization sees for its **own** clusters: active clusters,
+their licenses and licensed products, and per-cluster / per-license / per-resource
+event counts.
+
+All paths on this page are relative to `/api/v1`. Every endpoint requires
+`Authorization: token `, resolves the owner from the `org` query
+parameter (`?org=`), and requires the `view:contracts` permission on that
+organization (this is the org's "hosted mode / view contracts" grant — not
+site-admin). This group is available only on **billing-enabled deployments**.
+
+> **Verified:** every endpoint on this page returned `404 Not Found` against
+> `appscode` on `` on 2026-07-14 — this deployment is not billing-enabled,
+> so the `/dashboard/clusters/*` routes are not registered. (Sanity: `GET /version`
+> and `GET /user` returned `200` with the same token.)
+
+Shared conventions on this page:
+
+- **`cid`** is a cluster UID string; **`lid`** is a license ID string; **`product`**
+ is a product name.
+- **`limit`** (query, optional) sets the "active" window: a Go duration such as
+ `720h` or a `YYYY-MM-DD` date; defaults to roughly the last six months.
+
+## Clusters
+
+### GET /dashboard/clusters/active
+
+Lists clusters that reported within the `limit` window for the owner resolved from
+the query.
+
+- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | yes | Organization slug that owns the clusters. |
+| `limit` | string | no | Activity window (Go duration or `YYYY-MM-DD` date). |
+
+- **Response:** `200 OK` — an array of `ActiveClustersAPIForm`.
+
+```json
+[
+ {
+ "username": "appscode",
+ "clusterID": "",
+ "firstReceivedAt": 1700000000,
+ "lastReceivedAt": 1718000000,
+ "noOfAssociatedContracts": 2
+ }
+]
+```
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/dashboard/clusters/active?org=appscode"
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled on this deployment.
+
+### GET /dashboard/clusters/{cid}
+
+Returns cluster information for a cluster owned by the request owner.
+
+- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `cid` | string | Cluster UID. |
+
+- **Response:** `200 OK` — a single `ClusterInfo` object (the same shape documented
+ on the [Admin Billing Dashboard](../admin-dashboard.md) page;
+ `status` is a free-form Kubernetes-style object). `404` if the cluster is unknown
+ or not owned by the org.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/clusters/{cid}/events-count
+
+Returns today's event count for the cluster.
+
+- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
+- **Path parameters:** `cid` (string) — cluster UID.
+- **Response:** `200 OK` — an `EventsCounterResponse`.
+
+```json
+{
+ "clusterID": "",
+ "date": "2026-07-14",
+ "noOfEvents": 27
+}
+```
+
+The `product`, `licenseID`, `group`, `resource`, and `RID` fields are populated on
+the license/resource-scoped counter endpoints below; `error` is set instead of
+`noOfEvents` when the count could not be computed.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## Licenses
+
+### GET /dashboard/clusters/{cid}/licenses/
+
+Lists licensed plans associated with the request owner and cluster.
+
+- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
+- **Path parameters:** `cid` (string) — cluster UID.
+- **Response:** `200 OK` — an array of `LicensedPlan` (see the Admin Billing
+ Dashboard page for the full shape).
+
+```json
+[
+ {
+ "uid": 42,
+ "email": "user@example.com",
+ "licenseID": "",
+ "clusters": [""],
+ "planName": "kubedb-enterprise",
+ "notBefore": 1700000000,
+ "notAfter": 1731600000
+ }
+]
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/clusters/{cid}/licenses/{lid}
+
+Returns a licensed plan (API form) associated with the request owner.
+
+- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
+- **Path parameters:** `cid` (string), `lid` (string) — license ID.
+- **Response:** `200 OK` — a `LicensePlanApiForm`.
+
+```json
+{
+ "uid": 42,
+ "email": "user@example.com",
+ "licenseID": "",
+ "clusters": [""],
+ "planName": "kubedb-enterprise",
+ "notBefore": 1700000000,
+ "notAfter": 1731600000,
+ "reconciledAt": 1718000000,
+ "registeredAt": 1700000000,
+ "createdAt": 1700000000,
+ "updatedAt": 1718000000
+}
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+## Event counts and events
+
+### GET /dashboard/clusters/{cid}/licenses/{lid}/products/{product}/events-count
+
+Returns today's event count for a license/product.
+
+- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
+- **Path parameters:** `cid` (string), `lid` (string), `product` (string).
+- **Response:** `200 OK` — an `EventsCounterResponse` (with `product` and
+ `licenseID` populated).
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/clusters/{cid}/licenses/{lid}/products/{product}/groups/{group}/resources/{resource}/{rid}/events-count
+
+Returns today's event count for a specific resource object.
+
+- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
+- **Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `cid` | string | Cluster UID. |
+| `lid` | string | License ID. |
+| `product` | string | Product name. |
+| `group` | string | Kubernetes API group. |
+| `resource` | string | Kubernetes resource. |
+| `rid` | string | Resource ID (object UID). |
+
+- **Response:** `200 OK` — an `EventsCounterResponse` (with `group`, `resource`, and
+ `RID` populated).
+
+```json
+{
+ "clusterID": "",
+ "product": "kube-db",
+ "licenseID": "",
+ "group": "kubedb.com",
+ "resource": "mongodbs",
+ "RID": "",
+ "date": "2026-07-14",
+ "noOfEvents": 4
+}
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/clusters/{cid}/licenses/{lid}/products/{product}/events/
+
+Returns the tabular event list for the caller's cluster/license/product.
+
+- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
+- **Path parameters:** `cid` (string), `lid` (string), `product` (string).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `lastDays` | integer | no | Look-back window in days (max 14; defaults to 14). |
+
+- **Response:** `200 OK` — an array of `ClusterResourceTabularForm`, newest first.
+
+```json
+[
+ {
+ "key": "",
+ "version": 3,
+ "timestamp": 1718000000,
+ "partialResource": {
+ "partialObjectMeta": { "...": "free-form partial Kubernetes object metadata" }
+ }
+ }
+]
+```
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
+
+### GET /dashboard/clusters/{cid}/licenses/{lid}/products/{product}/events/raw-event
+
+Returns the raw badger value for a specific event key/version.
+
+- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
+- **Path parameters:** `cid` (string), `lid` (string), `product` (string).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `key` | string | yes | Badger event key. |
+| `version` | integer | yes | Event version (uint). |
+
+- **Response:** `200 OK` — a single `BadgerValue`.
+
+```json
+{
+ "resourceID": { "...": "free-form Kubernetes object" },
+ "licenseID": "",
+ "version": 3,
+ "timestamp": 1718000000
+}
+```
+
+`400` when `key`/`version` are missing or invalid.
+
+> **Verified:** returned `404` against `appscode` — billing not enabled.
diff --git a/docs/platform/api/chart-repositories/_index.md b/docs/platform/api/chart-repositories/_index.md
new file mode 100644
index 0000000..d814f41
--- /dev/null
+++ b/docs/platform/api/chart-repositories/_index.md
@@ -0,0 +1,30 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-chart-repositories
+ name: Chart Repositories
+ parent: api
+ weight: 170
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Chart Repositories
+
+The Chart Repositories API exposes ACE's read-only view over the public Helm Hub
+chart repository index. It lets you enumerate the known Helm chart repositories,
+load an individual repository and list the charts it contains, and inspect the
+available versions of a specific chart.
+
+All endpoints live under the `/api/v1/chartrepositories` prefix and are
+**public** — they carry no `security` requirement in the API definition and can
+be called without organization context. In practice clients still send their
+bearer token on every request (the platform accepts it), but no membership or
+role is required. Only `GET` requests are exposed here.
+
+## Pages
+
+- [Chart Repositories](../chart-repositories.md) — list the
+ known chart repositories, list the charts inside a repository, and list the
+ versions of a named chart.
diff --git a/docs/platform/api/chart-repositories/chart-repositories.md b/docs/platform/api/chart-repositories/chart-repositories.md
new file mode 100644
index 0000000..3191ac4
--- /dev/null
+++ b/docs/platform/api/chart-repositories/chart-repositories.md
@@ -0,0 +1,206 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-chart-repositories-chart-repositories
+ name: Chart Repositories
+ parent: api-chart-repositories
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Chart Repositories
+
+These endpoints provide a read-only view over the public Helm Hub chart
+repository index used by ACE. You can list the known repositories, load a
+repository by URL and list the charts inside it, and list the versions of a
+named chart.
+
+All paths on this page are relative to `/api/v1`. The full base path is
+`/api/v1/chartrepositories`.
+
+All three endpoints are **public** (no `security` requirement, no organization
+context). Clients typically still send their bearer token, but no membership or
+role is required. Only `GET` is supported.
+
+Examples below use a placeholder host `` and a `$ACE_TOKEN`
+environment variable holding the caller's token.
+
+---
+
+## List chart repositories
+
+### GET /chartrepositories
+
+Returns the list of known Helm Hub chart repositories.
+
+- **Auth:** public. No organization context required.
+
+**Path parameters:** none.
+
+**Query parameters:** none.
+
+**Response:** `200 OK` — a JSON array of chart repository entries. Each entry is
+a Helm repository configuration (Helm's `repo.Entry`). In practice only `name`
+and `url` are populated for the public Helm Hub index; the credential-related
+fields are part of the schema but are empty for public repositories.
+
+```json
+[
+ { "name": "appscode", "url": "https://charts.appscode.com/stable" },
+ { "name": "bitnami", "url": "https://charts.bitnami.com/bitnami" },
+ { "name": "argo", "url": "https://argoproj.github.io/argo-helm" }
+]
+```
+
+Fields of each `ChartRepositoryEntry`:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `name` | string | Repository name. |
+| `url` | string | Repository index URL. |
+| `username` | string | Basic-auth username (empty for public repos). |
+| `password` | string | Basic-auth password (empty for public repos). |
+| `certFile` | string | Client TLS certificate file. |
+| `keyFile` | string | Client TLS key file. |
+| `caFile` | string | CA bundle file. |
+| `insecure_skip_tls_verify` | boolean | Skip TLS verification for the repo. |
+| `pass_credentials_all` | boolean | Pass credentials to all hosts. |
+
+**Example:**
+
+```bash
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/chartrepositories
+```
+
+> **Verified:** `GET` returned `200` on 2026-07-14 (public endpoint; not
+> cluster-scoped). Returned a large array of repositories; live entries populate
+> only `name` and `url`.
+
+---
+
+## List charts in a repository
+
+### GET /chartrepositories/charts
+
+Loads the chart repository at the given `url` and returns the names of the charts
+it contains.
+
+- **Auth:** public. No organization context required.
+
+**Path parameters:** none.
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `url` | string | yes | URL of the chart repository to load (for example `https://charts.appscode.com/stable`). |
+
+**Response:** `200 OK` — a JSON array of chart names (strings) found in the
+repository index.
+
+```json
+[
+ "kubedb",
+ "stash",
+ "cert-manager-crds",
+ "prometheus"
+]
+```
+
+Other statuses:
+
+| Status | Meaning |
+|--------|---------|
+| `400` | Missing chart repo `url`. |
+| `500` | Internal server error (for example the repository index could not be loaded). |
+
+**Example:**
+
+```bash
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/chartrepositories/charts?url=https://charts.appscode.com/stable"
+```
+
+> **Verified:** `GET` returned `200` on 2026-07-14 for
+> `url=https://charts.appscode.com/stable` (public endpoint; not cluster-scoped).
+> Response was a JSON array of chart-name strings (264 charts).
+
+---
+
+## List versions of a chart
+
+### GET /chartrepositories/charts/{name}/versions
+
+Loads the chart repository at the given `url` and returns the available versions
+of the named chart.
+
+- **Auth:** public. No organization context required.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `name` | string | Name of the chart (for example `kubedb`). |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `url` | string | yes | URL of the chart repository that contains the chart. |
+
+**Response:** `200 OK` — a JSON array of chart version entries (Helm's
+`repo.ChartVersion` / chart metadata). The schema models the common chart
+metadata fields; a given repository only populates the fields it publishes. In
+practice the public Helm Hub index returns `version`, `appVersion`, `created`,
+and (when set) `kubeVersion`.
+
+```json
+[
+ {
+ "version": "v2026.7.10",
+ "appVersion": "v2026.7.10",
+ "kubeVersion": ">=1.26.0-0",
+ "created": "2026-07-13T07:15:34.541887Z"
+ }
+]
+```
+
+Fields of each `ChartVersion` (as defined by the API schema):
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `name` | string | Chart name. |
+| `version` | string | Chart version (SemVer). |
+| `description` | string | Chart description. |
+| `apiVersion` | string | Chart API version (`v1` or `v2`). |
+| `appVersion` | string | Version of the application packaged by the chart. |
+| `type` | string | Chart type (`application` or `library`). |
+| `deprecated` | boolean | Whether this chart version is deprecated. |
+| `icon` | string | URL of the chart icon. |
+| `home` | string | Home page URL of the project. |
+| `keywords` | array of string | Search keywords. |
+| `sources` | array of string | Source URLs for the project. |
+| `urls` | array of string | Download URLs for the chart archive. |
+| `created` | string (date-time) | Publication timestamp. |
+| `digest` | string | Digest of the chart archive. |
+
+Other statuses:
+
+| Status | Meaning |
+|--------|---------|
+| `400` | Missing chart repo `url` or chart `name`. |
+| `500` | Internal server error (for example the repository index could not be loaded). |
+
+**Example:**
+
+```bash
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/chartrepositories/charts/kubedb/versions?url=https://charts.appscode.com/stable"
+```
+
+> **Verified:** `GET` returned `200` on 2026-07-14 for chart `kubedb` with
+> `url=https://charts.appscode.com/stable` (public endpoint; not cluster-scoped).
+> Response was a JSON array of 106 version entries.
diff --git a/docs/platform/api/client-organizations/_index.md b/docs/platform/api/client-organizations/_index.md
new file mode 100644
index 0000000..b42e1f1
--- /dev/null
+++ b/docs/platform/api/client-organizations/_index.md
@@ -0,0 +1,35 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-client-organizations
+ name: Client Organizations
+ parent: api
+ weight: 80
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Client Organizations
+
+These endpoints support the **managed-service-provider** model. A platform site
+admin creates dedicated "client organizations" (a specialized kind of ACE
+organization), imports spoke clusters into them, and then manages per-cluster
+user access on behalf of each client.
+
+There are two concerns, split across two pages:
+
+- [Client Org Management](../management.md) — site-admin
+ lifecycle of client organizations: list/get/create/delete client orgs, add or
+ remove clusters, and query client-org status. Routes live under
+ `/api/v1/user/client*`.
+- [Cluster User Permissions](../cluster-user-permissions.md)
+ — organization-admin management of the OCM users belonging to a client org on
+ a specific cluster: list users, create a user with permissions, inspect and
+ update permissions, generate a kubeconfig, and remove a user. Routes live
+ under `/api/v1/clusters/{owner}/{cluster}/permission/*`.
+
+All routes are served under the `/api/v1` prefix and require a personal access
+token (`Authorization: token `). Client-org **management** routes are
+site-admin only; the per-cluster **permission** routes require organization-admin
+authorization on the `owner` (which must resolve to a client organization).
diff --git a/docs/platform/api/client-organizations/cluster-user-permissions.md b/docs/platform/api/client-organizations/cluster-user-permissions.md
new file mode 100644
index 0000000..339a70b
--- /dev/null
+++ b/docs/platform/api/client-organizations/cluster-user-permissions.md
@@ -0,0 +1,285 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-client-organizations-cluster-user-permissions
+ name: Cluster User Permissions
+ parent: api-client-organizations
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Cluster User Permissions
+
+Manage the OCM users belonging to a client organization on a specific cluster:
+list them, create a user with permissions, inspect and update those permissions,
+generate a kubeconfig, remove specific grants, and delete the user entirely.
+
+All paths on this page are relative to the API root **`/api/v1`** and require a
+personal access token:
+
+```
+Authorization: token
+```
+
+**Path parameters common to every endpoint here:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `owner` | string | Organization slug that owns the cluster. **Must resolve to a client organization** (`reqClientOrg`); otherwise the request is rejected. |
+| `cluster` | string | Cluster name within the `owner` scope. |
+
+**Auth:** `GET /permission/users` requires a valid token and that `owner` is a
+client org. Every other endpoint additionally requires **organization-admin**
+authorization on `owner` (`Organization_Admin`).
+
+Illustrative curl calls use a placeholder host `` and `$ACE_TOKEN`.
+
+---
+
+### GET /clusters/{owner}/{cluster}/permission/users
+
+List the users of the client organization (`owner`) that have been granted access
+on this cluster.
+
+- **Auth:** token; `owner` must resolve to a client organization.
+
+**Response:** `200 OK` — a JSON array of `User` objects.
+
+```json
+[
+ {
+ "id": 101,
+ "login": "acme-admin",
+ "username": "acme-admin",
+ "full_name": "Acme Admin",
+ "email": "user@example.com",
+ "active": true,
+ "orgAdmin": true,
+ "clientOrgUser": true
+ }
+]
+```
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/acme-client/acme-prod/permission/users
+```
+
+> **Verified:** returned `500` against `appscode/ace` — `appscode` is not a client
+> organization on this platform (`user isn't a client organization`), so the
+> `reqClientOrg` check fails. Expected `200` with a `User` array when `owner` is a
+> real client org.
+
+---
+
+### POST /clusters/{owner}/{cluster}/permission/user/create
+
+Create an OCM user for the client organization on this cluster and assign the
+requested permissions (creating the necessary RBAC role / cluster-role bindings and
+importing/connecting clusters for the user).
+
+- **Auth:** token; **organization-admin** on `owner`.
+
+**Request body:** `UserOptions` (see [below](#useroptions)).
+
+```json
+{
+ "userName": "acme-dev",
+ "email": "user@example.com",
+ "permissions": [
+ {
+ "name": "acme-prod",
+ "isClusterSet": false,
+ "roleType": "ClusterRole",
+ "roleRefName": "ace:admin",
+ "roleRefNamespace": ["team-a"]
+ }
+ ],
+ "importCluster": true,
+ "clientOrg": true
+}
+```
+
+**Responses:** `200` user created and permissions assigned · `400` invalid
+permission options for the client org · `422` validation error.
+
+---
+
+### POST /clusters/{owner}/{cluster}/permission/user/{id}
+
+Get the roles and cluster-roles assigned to a client-org user on this cluster,
+scoped to the namespaces supplied in the request body. (This is a `POST` because it
+takes a namespace-scope selector in the body, but it is a read operation.)
+
+- **Auth:** token; **organization-admin** on `owner`.
+
+**Path parameters:** `owner`, `cluster` (above), plus:
+
+| Name | Type | Description |
+|------|------|-------------|
+| `id` | integer (int64) | Numeric ID of the user. |
+
+**Request body:** `PermissionScopes`.
+
+```json
+{
+ "namespaceList": ["team-a", "team-b"],
+ "allNamespaces": false
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `namespaceList` | string[] | no | Namespaces to scope the query to. |
+| `allNamespaces` | boolean | no | When true, return permissions across all namespaces. |
+
+**Response:** `200 OK` — a `ClientOrgUserPermission` object. `400` if the user is
+not part of the client org.
+
+```json
+{
+ "ClusterRoles": [
+ {
+ "ResourceName": "acme-dev-binding",
+ "ResourceNamespace": "",
+ "RoleName": "ace:admin",
+ "RoleNamespace": ""
+ }
+ ],
+ "Roles": [
+ {
+ "ResourceName": "acme-dev-team-a-binding",
+ "ResourceNamespace": "team-a",
+ "RoleName": "ace:editor",
+ "RoleNamespace": "team-a"
+ }
+ ]
+}
+```
+
+> Field names are capitalized because the source Go structs (`ocm.ClientOrgUserPermission`,
+> `ocm.ClientOrgUserPermissionDetails`) carry no JSON tags.
+
+---
+
+### GET /clusters/{owner}/{cluster}/permission/user/{id}/kubeconfig
+
+Generate a kubeconfig for a client-org user to access this cluster, provided the
+user has permission in the client organization's cluster.
+
+- **Auth:** token; **organization-admin** on `owner`.
+
+**Path parameters:** `owner`, `cluster`, plus:
+
+| Name | Type | Description |
+|------|------|-------------|
+| `id` | integer (int64) | Numeric ID of the user. |
+
+**Response:** `200 OK` — the generated kubeconfig, returned as a JSON string
+containing a YAML document. `400` if the user does not have permission in the
+client-org cluster.
+
+```json
+"apiVersion: v1\nkind: Config\nclusters:\n- cluster:\n server: https://\n name: acme-prod\n..."
+```
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/acme-client/acme-prod/permission/user/101/kubeconfig
+```
+
+> **Verified:** not called live — requires a real client-org user ID, which does
+> not exist on this platform (no client orgs). Documented from schema.
+
+---
+
+### POST /clusters/{owner}/{cluster}/permission/user/{id}/update
+
+Re-assign a client-org user's permissions on this cluster.
+
+- **Auth:** token; **organization-admin** on `owner`.
+
+**Path parameters:** `owner`, `cluster`, plus `id` (integer, int64 — the user ID).
+
+**Request body:** `UserOptions` (see [below](#useroptions)) — same shape as
+`user/create`.
+
+**Responses:** `200` permissions updated · `400` invalid permission options ·
+`404` user not found · `422` validation error.
+
+---
+
+### POST /clusters/{owner}/{cluster}/permission/user/{id}/remove
+
+Delete the named cluster-scoped and namespace-scoped RBAC bindings for a client-org
+user on this cluster (removes specific grants without deleting the user).
+
+- **Auth:** token; **organization-admin** on `owner`.
+
+**Path parameters:** `owner`, `cluster`, plus `id` (integer, int64 — the user ID).
+
+**Request body:** `RemovePermissionOpts`.
+
+```json
+{
+ "clusterScopedResources": ["acme-dev-binding"],
+ "namespaceScopedResources": [
+ { "Namespace": "team-a", "Name": "acme-dev-team-a-binding" }
+ ]
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `clusterScopedResources` | string[] | no | Names of cluster-scoped RBAC bindings to delete. |
+| `namespaceScopedResources` | `NamespacedName[]` | no | Namespaced bindings to delete. Each item marshals with capitalized keys `Namespace` and `Name` (the source struct has no JSON tags). |
+
+**Responses:** `200` permissions removed · `404` user not found · `422` validation
+error.
+
+---
+
+### DELETE /clusters/{owner}/{cluster}/permission/user/{id}/delete
+
+Remove a client-org user entirely from this cluster: strips the hub-owner and
+client-org annotations and deletes all associated RBAC role and cluster-role
+bindings.
+
+- **Auth:** token; **organization-admin** on `owner`.
+
+**Path parameters:** `owner`, `cluster`, plus `id` (integer, int64 — the user ID).
+
+**Responses:** `200` user removed from the cluster · `404` user not found.
+
+---
+
+## Schemas
+
+### UserOptions
+
+Options for creating or updating an OCM user's permissions. Used by
+`user/create`, `user/{id}/update`, and by the `userPermission` field of the
+client-org create call.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | integer (int64) | User ID (for updates). |
+| `userName` | string | The user's username. |
+| `email` | string | The user's email. |
+| `permissions` | `Permission[]` | Cluster / cluster-set permission grants (see below). |
+| `importCluster` | boolean | Import/connect the referenced clusters for the user. |
+| `clientOrg` | boolean | Whether this user belongs to a client organization. |
+
+**Permission** — a single cluster or cluster-set permission grant:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `name` | string | Cluster or cluster-set name. |
+| `isClusterSet` | boolean | True if `name` refers to a cluster set. |
+| `roleType` | string | Role scope: `ClusterRole` or `Role`. |
+| `roleRefName` | string | Name of the referenced (cluster-)role. |
+| `roleRefNamespace` | string[] | Namespaces the role applies to (for `Role` scope). |
+| `managedClusterRoleBindingName` | string | Name of the generated managed cluster role binding. |
+| `managedClusterSetRoleBindingName` | string | Name of the generated managed cluster-set role binding. |
diff --git a/docs/platform/api/client-organizations/management.md b/docs/platform/api/client-organizations/management.md
new file mode 100644
index 0000000..c8c826e
--- /dev/null
+++ b/docs/platform/api/client-organizations/management.md
@@ -0,0 +1,328 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-client-organizations-management
+ name: Client Org Management
+ parent: api-client-organizations
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Client Org Management
+
+Site-admin lifecycle of client organizations: list them, fetch one, create a new
+client org (which also provisions its admin user and imports clusters), add or
+remove clusters, query status, and delete a client org.
+
+All paths on this page are relative to the API root **`/api/v1`** and require a
+personal access token:
+
+```
+Authorization: token
+```
+
+Every endpoint here is **site-admin only** — the caller must be a platform site
+admin acting within an organization. Non-admin callers receive `403`.
+
+Illustrative curl calls use a placeholder host `` and `$ACE_TOKEN`.
+
+> **Platform note:** On the verification platform there are currently **no client
+> organizations** — `GET /user/clients` returns an empty list (`[]`, HTTP `200`).
+> An empty/`null` list is a valid, verified success response.
+
+---
+
+## List & inspect
+
+### GET /user/clients
+
+List all client organizations known to the platform.
+
+- **Auth:** token; **site-admin only** (`Organization_SiteAdminCanViewClientOrg`).
+ The caller must be acting within an organization (`reqMustFromOrg`).
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `org` | string | recommended | Organization slug providing the caller's org context. |
+
+**Response:** `200 OK` — a JSON array of `Organization` objects. Empty when no
+client orgs exist.
+
+```json
+[
+ {
+ "id": 42,
+ "username": "acme-client",
+ "full_name": "Acme Client Org",
+ "description": "Managed client organization",
+ "website": "https://example.com",
+ "location": "",
+ "rancherManagementClusterEndPoint": "",
+ "visibility": "private",
+ "orgType": 2
+ }
+]
+```
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/user/clients?org=appscode"
+```
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14; the platform
+> has no client orgs, so the body was `[]` (a valid empty result).
+
+---
+
+### GET /user/client/{id}
+
+Fetch a single client organization by its numeric ID.
+
+- **Auth:** token; **site-admin only** (`Organization_SiteAdminCanViewClientOrg`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `id` | integer (int64) | Numeric ID of the client organization. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `org` | string | recommended | Caller's organization context. |
+
+**Response:** `200 OK` — a single `Organization` object (same shape as the list
+items above). `404` if no client org has that ID.
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/user/client/42?org=appscode"
+```
+
+> **Verified:** returned `500` against `appscode` — no client org exists with the
+> probed ID on this platform (`user does not exist`). Expected to return `200`
+> with an `Organization` once a client org exists.
+
+---
+
+### GET /user/client/{orgname}/status
+
+Aggregate deletion/active status across all clusters of the client organization.
+
+- **Auth:** token; **site-admin only** (`Organization_SiteAdminCanViewClientOrg`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `orgname` | string | Client organization slug. |
+
+**Response:** `200 OK` — a `ClientOrgStatus` envelope. `status` is one of
+`Active`, `Deleting`, `Partially Deleted`, `Deleted`, or `NotFound`.
+
+```json
+{
+ "status": "Active"
+}
+```
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/user/client/acme-client/status?org=appscode"
+```
+
+> **Verified:** returned `500` against `appscode` — `appscode` is not itself a
+> client org (`org does not exist`). Expected `200` with a `ClientOrgStatus` for a
+> real client-org slug.
+
+---
+
+### GET /user/client/{orgname}/cluster/{cluster}/status
+
+Deletion/active status of a single cluster within the client organization.
+
+- **Auth:** token; **site-admin only** (`Organization_SiteAdminCanViewClientOrg`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `orgname` | string | Client organization slug. |
+| `cluster` | string | Cluster name within the client organization. |
+
+**Response:** `200 OK` — a `ClientOrgStatus` envelope (see above).
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/user/client/acme-client/cluster/acme-prod/status?org=appscode"
+```
+
+> **Verified:** returned `500` against `appscode/ace` — `appscode` is not a client
+> org on this platform. Expected `200` with a `ClientOrgStatus` for a real
+> client-org cluster.
+
+---
+
+## Mutations
+
+The endpoints below change state. They are documented from the OpenAPI schema and
+were **not** called against the live platform.
+
+### POST /user/client/create
+
+Create a client organization. This also provisions the org admin user, grants that
+user permission on the hub cluster, and imports the requested spoke cluster(s) with
+the client org as owner.
+
+- **Auth:** token; **site-admin only** (`Organization_SiteAdminCanCreateClientOrg`).
+
+**Request body:** `ClientOrgParams`.
+
+```json
+{
+ "clientOrg": {
+ "orgName": "acme-client",
+ "orgFullName": "Acme Client Org",
+ "description": "Managed client organization",
+ "website": "https://example.com",
+ "orgType": "client",
+ "visibility": "private"
+ },
+ "hubClusterUID": "",
+ "hubClusterOwnerName": "appscode",
+ "userPermission": {
+ "userName": "acme-admin",
+ "email": "user@example.com",
+ "permissions": [
+ {
+ "name": "acme-prod",
+ "isClusterSet": false,
+ "roleType": "ClusterRole",
+ "roleRefName": "ace:admin"
+ }
+ ],
+ "importCluster": true,
+ "clientOrg": true
+ },
+ "useGateway": true
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `clientOrg` | `CreateOrgParams` | yes | The organization to create (name, full name, type, visibility, optional Rancher sync token, etc.). |
+| `hubClusterUID` | string | yes | UID of the hub cluster imported by the site admin. |
+| `hubClusterOwnerName` | string | yes | Owner (org slug) of the hub cluster. |
+| `userPermission` | `UserOptions` | yes | The org-admin user to provision and the permissions to grant (see [Cluster User Permissions](../cluster-user-permissions.md#useroptions)). |
+| `annotations` | map[string]string | no | Annotations applied to created resources. |
+| `kubeDBConfig` | `KubeDBConfig` | no | KubeDB scheduling config (node selector, tolerations). |
+| `useGateway` | boolean | no | Provision a gateway for the client org. |
+| `usableAsShared` | boolean | no | Whether the org's gateway config may be shared by other orgs. |
+| `gatewayConfig` / `gatewayConfigRef` | object | no | Inline gateway config or a reference to an existing one (Kubernetes object). |
+| `telemetryParams` | `TelemetryParams` | no | Monitoring cluster and retention periods. |
+
+**Responses:** `200` created · `400` invalid params (e.g. missing Rancher
+management cluster endpoint) · `409` an org with the same name already exists ·
+`422` validation error.
+
+---
+
+### POST /user/client/{orgname}/add-cluster
+
+Import an additional spoke cluster into a client organization, granting the org
+admins access and setting up namespaces, gateway presets, and telemetry.
+
+- **Auth:** token; **site-admin only** (`Organization_SiteAdminCanAddClusterToClientOrg`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `orgname` | string | Client organization slug. |
+
+**Request body:** `AddClusterParams`.
+
+```json
+{
+ "hubClusterOwnerName": "appscode",
+ "hubClusterName": "ace",
+ "hubClusterUID": "",
+ "spokeClusterName": "acme-staging",
+ "useGateway": true
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `hubClusterOwnerName` | string | yes | Owner (org slug) of the hub cluster. |
+| `hubClusterName` | string | yes | Hub cluster name. |
+| `hubClusterUID` | string | yes | Hub cluster UID. |
+| `spokeClusterName` | string | yes | Spoke cluster to import into the client org. |
+| `annotations` | map[string]string | no | Annotations applied to created resources. |
+| `kubeDBConfig` | `KubeDBConfig` | no | KubeDB scheduling config. |
+| `useGateway` | boolean | no | Provision a gateway for the added cluster. |
+| `gatewayConfig` / `gatewayPresetRef` | object | no | Inline gateway config or a preset reference. |
+| `telemetryParams` | `TelemetryParams` | no | Monitoring cluster and retention periods. |
+
+**Responses:** `200` added · `400` the cluster is already imported for this org ·
+`422` validation error.
+
+---
+
+### POST /user/client/{orgname}/delete-cluster
+
+Remove a spoke cluster from a client organization, cleaning up the client-org
+namespace and associated hub/spoke resources.
+
+- **Auth:** token; **site-admin only** (`Organization_SiteAdminCanRemoveClusterFromClientOrg`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `orgname` | string | Client organization slug. |
+
+**Request body:** `RemoveClusterParams`.
+
+```json
+{
+ "hubClusterOwnerName": "appscode",
+ "hubClusterName": "ace",
+ "hubClusterUID": "",
+ "spokeClusterName": "acme-staging"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `hubClusterOwnerName` | string | yes | Owner (org slug) of the hub cluster. |
+| `hubClusterName` | string | yes | Hub cluster name. |
+| `hubClusterUID` | string | yes | Hub cluster UID. |
+| `spokeClusterName` | string | yes | Spoke cluster to remove. |
+
+**Responses:** `200` removal accepted (cleanup runs asynchronously) · `422`
+validation error.
+
+---
+
+### DELETE /user/client/delete/{id}
+
+Delete a client organization, tearing down its namespaces and cluster resources
+across all spoke clusters. Rejected if another organization still references this
+client org's gateway config.
+
+- **Auth:** token; **site-admin only** (`Organization_SiteAdminCanDeleteClientOrg`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `id` | integer (int64) | Numeric ID of the client organization. |
+
+**Responses:** `200` deletion accepted (teardown runs asynchronously) · `400` the
+client org is still in use by another organization · `404` not found.
diff --git a/docs/platform/api/cloud-providers/_index.md b/docs/platform/api/cloud-providers/_index.md
new file mode 100644
index 0000000..aed604b
--- /dev/null
+++ b/docs/platform/api/cloud-providers/_index.md
@@ -0,0 +1,34 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-cloud-providers
+ name: Cloud Providers
+ parent: api
+ weight: 90
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Cloud Providers
+
+Discovery APIs used by the cluster-provisioning wizard. Given stored cloud
+credentials for an owner (organization or user), these endpoints enumerate the
+projects, regions, zones, Kubernetes versions, machine types, and existing
+managed clusters available on each supported cloud provider — so the console can
+offer them as choices when provisioning or importing a cluster. A single public
+endpoint lists the providers the platform supports, and a provisioning endpoint
+creates a new Cluster API (CAPI) cluster on a provider.
+
+All routes are served under the `/api/v1` prefix. Except for the public provider
+list (`GET /clouds`), every endpoint requires a personal access token sent as
+`Authorization: token ` (it may also be supplied as a `token` or
+`access_token` query parameter). The per-provider discovery routes additionally
+require **stored cloud credentials** for the referenced provider under the owner
+scope; without a matching credential these routes fail.
+
+## Pages
+
+- [Cloud Providers](../cloud-providers.md) — the public provider
+ list, CAPI cluster provisioning, and per-provider discovery for GKE, AKS, EKS,
+ DigitalOcean, Linode, Rancher, Hetzner, and KubeVirt.
diff --git a/docs/platform/api/cloud-providers/cloud-providers.md b/docs/platform/api/cloud-providers/cloud-providers.md
new file mode 100644
index 0000000..9f6d91b
--- /dev/null
+++ b/docs/platform/api/cloud-providers/cloud-providers.md
@@ -0,0 +1,598 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-cloud-providers-cloud-providers
+ name: Cloud Providers
+ parent: api-cloud-providers
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Cloud Providers
+
+Discovery and provisioning APIs for cloud Kubernetes providers. All paths on this
+page are relative to the API base **`/api/v1`** — for example the discovery route
+written `GET /clouds/{owner}/providers/gke/projects` is served at
+`/api/v1/clouds/{owner}/providers/gke/projects`.
+
+Except for the public provider list, every endpoint requires a personal access
+token:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clouds/appscode/providers/eks/regions
+```
+
+The per-provider discovery routes talk to the cloud provider's API using a
+**stored cloud credential** for the referenced provider under the `{owner}`
+scope. If no matching credential is stored for the owner (or the provider needs
+extra configuration, such as a configured Rancher client), the route returns an
+error. Cloud credentials are managed via the
+[user/organization credentials APIs](../../users-settings/authenticated-user.md).
+
+## Common path parameter
+
+Every owner-scoped route below takes an `{owner}` path parameter:
+
+| Name | Type | Description |
+|---|---|---|
+| `owner` | string | Organization slug or username that owns the stored cloud credentials. |
+
+Other path parameters (`project`, `region`, `resourcegroup`, `cluster`, `id`) are
+described per endpoint.
+
+## Common response shapes
+
+Several discovery endpoints share these response schemas.
+
+`Locations` — a cloud region with its availability zones:
+
+```json
+{
+ "region": "us-east1",
+ "description": "South Carolina",
+ "zones": ["us-east1-b", "us-east1-c", "us-east1-d"]
+}
+```
+
+`VMInfo` — a machine type / server type:
+
+```json
+{
+ "name": "e2-standard-4",
+ "description": "4 vCPU, 16 GB",
+ "cpu": 4,
+ "core": 2,
+ "memoryMb": 16384,
+ "disksSizeGb": 0,
+ "iops": 0,
+ "shared": false,
+ "baremetal": false,
+ "gpus": 0,
+ "category": "general-purpose",
+ "family": "e2",
+ "instanceHypervisor": "kvm",
+ "architecture": ["amd64"],
+ "capacityType": ["on-demand", "spot"],
+ "networkBW": "up to 10 Gbps",
+ "ebsBW": "",
+ "monthlyCost": "97.09"
+}
+```
+
+`ClusterInfo` — summary of a managed cluster on the provider (used when listing or
+inspecting clusters for import):
+
+```json
+{
+ "id": 0,
+ "displayName": "prod",
+ "name": "prod",
+ "uid": "",
+ "provider": "gce",
+ "vendor": "gke",
+ "endpoint": "https://34.x.x.x",
+ "location": "us-east1",
+ "project": "my-gcp-project",
+ "kubernetesVersion": "1.30.1",
+ "nodeCount": 3,
+ "createdAt": "2026-01-01T00:00:00Z",
+ "age": "6mo",
+ "clusterManagers": ["gke"],
+ "isCapiClusterUpgradeable": false,
+ "status": {}
+}
+```
+
+`ClusterInfo` fields include `id`/`uid`/`externalID` identifiers, `displayName`
+and `name`, provider metadata (`provider`, `vendor`, `endpoint`, `location`,
+`project`), `kubernetesVersion`, `nodeCount`, `createdAt`/`age`, and a
+Kubernetes-style `status` object. Not all fields are populated for every provider.
+
+---
+
+## Provider catalog
+
+### GET /clouds
+
+Lists the cloud providers the platform supports for cluster provisioning and
+discovery.
+
+- **Auth:** public — no authentication required.
+- **Path parameters:** none.
+- **Query parameters:** none.
+
+**Response** `200` — a `CloudProviderList` (a Kubernetes-style list whose `items`
+carry the provider name in `metadata.name`):
+
+```json
+{
+ "metadata": {},
+ "items": [
+ { "metadata": { "name": "gce" }, "spec": {} },
+ { "metadata": { "name": "digitalocean" }, "spec": {} },
+ { "metadata": { "name": "packet" }, "spec": {} },
+ { "metadata": { "name": "aws" }, "spec": {} },
+ { "metadata": { "name": "azure" }, "spec": {} },
+ { "metadata": { "name": "vultr" }, "spec": {} },
+ { "metadata": { "name": "linode" }, "spec": {} },
+ { "metadata": { "name": "scaleway" }, "spec": {} }
+ ]
+}
+```
+
+Each item's `spec` may optionally carry `regions` (region + zones + location) and
+`machineTypes`, but is typically empty in the list response — use the per-provider
+discovery endpoints below to enumerate regions and machine types.
+
+> **Verified:** `GET` returned `200` against the live platform on 2026-07-14; the
+> response shown above is the sanitized live payload.
+
+---
+
+## Provisioning
+
+### POST /clouds/{owner}/{provider}/cluster
+
+Provisions a new Cluster API (CAPI) based cluster on the given cloud provider and
+imports it into the platform. For `kubevirt` the CAPI cluster config is validated
+(worker pools, machine count, cpu, memory are required).
+
+- **Auth:** token. Requires stored cloud credentials for the referenced provider
+ under the owner scope.
+- **Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `owner` | string | Organization slug or username that owns the cloud credentials. |
+| `provider` | string | Target cloud provider name (e.g. `gke`, `aks`, `eks`, `kubevirt`). |
+
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `response-id` | string | no | Correlation id for the asynchronous provisioning response. |
+
+**Request body** — a `ClusterProvisionConfig`:
+
+```json
+{
+ "capiClusterConfig": {
+ "clusterName": "demo",
+ "region": "us-east1",
+ "networkCIDR": "10.0.0.0/16",
+ "kubernetesVersion": "1.30.1",
+ "googleProjectID": "my-gcp-project",
+ "controlPlane": {
+ "machineType": "e2-standard-4",
+ "machineCount": 1,
+ "cpu": 4,
+ "memory": 16384
+ },
+ "workerPools": [
+ {
+ "machineType": "e2-standard-4",
+ "machineCount": 3,
+ "cpu": 4,
+ "memory": 16384
+ }
+ ]
+ },
+ "importOptions": {
+ "basicInfo": { "name": "demo", "displayName": "Demo" },
+ "provider": { "name": "gce", "credential": "my-gcp-cred" },
+ "components": { "fluxCD": true, "allFeatures": false }
+ }
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `capiClusterConfig` | object | yes | CAPI cluster configuration (see below). |
+| `capiClusterConfig.clusterName` | string | — | Name for the new cluster. |
+| `capiClusterConfig.region` | string | — | Provider region. |
+| `capiClusterConfig.networkCIDR` | string | — | Pod/network CIDR. |
+| `capiClusterConfig.kubernetesVersion` | string | — | Kubernetes version. |
+| `capiClusterConfig.googleProjectID` | string | — | GCP project id (GKE only). |
+| `capiClusterConfig.controlPlane` | object | — | Control-plane machine pool (`machineType`, `machineCount`, `cpu`, `memory` — all required within a pool). |
+| `capiClusterConfig.workerPools` | array | — | Worker machine pools; each pool requires `machineType`, `machineCount`, `cpu`, `memory`. |
+| `importOptions` | object | — | How to import the resulting cluster: `basicInfo`, `provider` (credential/region/project), and `components` (feature sets, FluxCD). |
+
+**Response** `200` — a `ProviderOptions` describing the provisioned cluster:
+
+```json
+{
+ "name": "demo",
+ "credential": "my-gcp-cred",
+ "clusterID": "",
+ "project": "my-gcp-project",
+ "region": "us-east1",
+ "kubeConfig": ""
+}
+```
+
+`ProviderOptions` fields: `name`, `credential`, `eksAuthMode` (`IRSA` or
+`PodIdentity`, EKS only), `clusterID`, `project`, `region`, `resourceGroup`, and
+`kubeConfig`. Error responses: `400` (invalid CAPI config, e.g. missing worker
+pools), `401` (unauthenticated), `403` (forbidden), `422` (body failed
+validation).
+
+> **Not verified:** write endpoint — not called against the live platform per
+> GET-only policy.
+
+---
+
+## GKE discovery
+
+Google Kubernetes Engine discovery routes. All are `GET`, token-authenticated, and
+require stored GKE credentials for `{owner}`.
+
+### GET /clouds/{owner}/providers/gke/projects
+
+Lists the Google Cloud projects accessible with the stored GKE credentials.
+
+- **Auth:** token + stored GKE credentials.
+- **Response** `200` — an array of project id strings: `["my-gcp-project", "..."]`.
+
+> **Verified:** returned `302` (redirect to `/500`) against `appscode` on
+> 2026-07-14 — no GKE cloud credential is stored for the `appscode` owner, so the
+> handler cannot reach the GCP API.
+
+### GET /clouds/{owner}/providers/gke/projects/{project}/regions/
+
+Lists available regions (with zones) for the given GKE project.
+
+- **Path parameters:** `project` (string) — GKE project id.
+- **Response** `200` — an array of [`Locations`](#common-response-shapes).
+
+> **Verified:** requires stored GKE credentials; not reachable for `appscode`
+> (same missing-credential condition as `.../gke/projects`).
+
+### GET /clouds/{owner}/providers/gke/projects/{project}/regions/{region}/kubernetesversions
+
+Lists the supported Kubernetes versions for the given GKE project and region.
+
+- **Path parameters:** `project` (string), `region` (string).
+- **Response** `200` — an array of version strings, e.g. `["1.29.5", "1.30.1"]`.
+
+> **Verified:** requires stored GKE credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/gke/projects/{project}/regions/{region}/vms
+
+Lists available machine types for the given GKE project, region and zone, grouped
+by family.
+
+- **Path parameters:** `project` (string), `region` (string).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `zone` | string | yes | GKE zone (e.g. `us-east1-b`). |
+
+- **Response** `200` — an object mapping family name → array of
+ [`VMInfo`](#common-response-shapes):
+
+```json
+{
+ "e2": [ { "name": "e2-standard-4", "cpu": 4, "memoryMb": 16384 } ],
+ "n2": [ { "name": "n2-standard-8", "cpu": 8, "memoryMb": 32768 } ]
+}
+```
+
+> **Verified:** requires stored GKE credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/gke/projects/{project}/clusters
+
+Lists the GKE clusters that exist in the given project.
+
+- **Path parameters:** `project` (string).
+- **Response** `200` — an array of [`ClusterInfo`](#common-response-shapes).
+
+> **Verified:** requires stored GKE credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/gke/projects/{project}/clusters/{cluster}
+
+Returns detailed information for a single GKE cluster (for import).
+
+- **Path parameters:** `project` (string), `cluster` (string) — GKE cluster name.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `location` | string | no | GKE cluster location. |
+
+- **Response** `200` — a [`ClusterInfo`](#common-response-shapes). Errors: `404`
+ (not found), `409` (already imported).
+
+> **Verified:** requires stored GKE credentials; not reachable for `appscode`.
+
+---
+
+## AKS discovery
+
+Azure Kubernetes Service discovery routes. All are `GET`, token-authenticated, and
+require stored AKS credentials for `{owner}`.
+
+### GET /clouds/{owner}/providers/aks/regions/
+
+Lists the supported Azure (AKS) regions.
+
+- **Response** `200` — an array of [`Locations`](#common-response-shapes).
+
+> **Verified:** returned `302` (redirect to `/500`) against `appscode` on
+> 2026-07-14 — no AKS cloud credential is stored for the `appscode` owner.
+
+### GET /clouds/{owner}/providers/aks/regions/{region}/vms
+
+Lists available machine types for the given AKS region, grouped by family.
+
+- **Path parameters:** `region` (string).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `zones` | string | yes | Comma-separated availability zones (e.g. `1,2,3`). |
+
+- **Response** `200` — an object mapping family name → array of
+ [`VMInfo`](#common-response-shapes).
+
+> **Verified:** requires stored AKS credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/aks/regions/{region}/kubernetesversions
+
+Lists the supported Kubernetes versions for the given AKS region.
+
+- **Path parameters:** `region` (string).
+- **Response** `200` — an array of version strings.
+
+> **Verified:** requires stored AKS credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/aks/resourcegroups
+
+Lists the Azure resource groups accessible with the stored AKS credentials.
+
+- **Response** `200` — an array of resource group name strings.
+
+> **Verified:** requires stored AKS credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/aks/resourcegroups/{resourcegroup}/clusters
+
+Lists the AKS managed clusters in the given Azure resource group.
+
+- **Path parameters:** `resourcegroup` (string).
+- **Response** `200` — an array of [`ClusterInfo`](#common-response-shapes).
+
+> **Verified:** requires stored AKS credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/aks/resourcegroups/{resourcegroup}/clusters/{cluster}
+
+Returns detailed information for a single AKS cluster (for import).
+
+- **Path parameters:** `resourcegroup` (string), `cluster` (string).
+- **Response** `200` — a [`ClusterInfo`](#common-response-shapes). Errors: `404`,
+ `409` (import conflict).
+
+> **Verified:** requires stored AKS credentials; not reachable for `appscode`.
+
+---
+
+## EKS discovery
+
+Amazon Elastic Kubernetes Service discovery routes. All are `GET`,
+token-authenticated, and require stored EKS credentials for `{owner}` (region
+listing may fall back to a default credential).
+
+### GET /clouds/{owner}/providers/eks/regions
+
+Lists the supported AWS (EKS) regions. If no credential is stored, a default
+credential may be used.
+
+- **Response** `200` — an array of [`Locations`](#common-response-shapes).
+
+> **Verified:** returned `302` (redirect to `/500`) against `appscode` on
+> 2026-07-14 — resolving AWS regions failed with no usable AWS credential for the
+> `appscode` owner.
+
+### GET /clouds/{owner}/providers/eks/regions/{region}/kubernetesversions
+
+Lists the supported Kubernetes versions for EKS.
+
+- **Path parameters:** `region` (string).
+- **Response** `200` — an array of version strings.
+
+> **Verified:** requires stored EKS credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/eks/regions/{region}/vms
+
+Lists available machine types for the given EKS region, grouped by family.
+
+- **Path parameters:** `region` (string).
+- **Response** `200` — an object mapping family name → array of
+ [`VMInfo`](#common-response-shapes).
+
+> **Verified:** requires stored EKS credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/eks/regions/{region}/clusters
+
+Lists the EKS clusters in the given AWS region.
+
+- **Path parameters:** `region` (string).
+- **Response** `200` — an array of [`ClusterInfo`](#common-response-shapes).
+
+> **Verified:** requires stored EKS credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/eks/regions/{region}/clusters/{cluster}
+
+Returns detailed information for a single EKS cluster (for import).
+
+- **Path parameters:** `region` (string), `cluster` (string).
+- **Response** `200` — a [`ClusterInfo`](#common-response-shapes). Errors: `404`,
+ `409` (import conflict).
+
+> **Verified:** requires stored EKS credentials; not reachable for `appscode`.
+
+---
+
+## DigitalOcean discovery
+
+DigitalOcean Kubernetes (DOKS) discovery routes. All are `GET`,
+token-authenticated, and require stored DigitalOcean credentials for `{owner}`.
+
+### GET /clouds/{owner}/providers/digitalocean/clusters
+
+Lists the DigitalOcean managed Kubernetes (DOKS) clusters.
+
+- **Response** `200` — an array of [`ClusterInfo`](#common-response-shapes).
+
+> **Verified:** returned `302` (redirect to `/500`) against `appscode` on
+> 2026-07-14 — no DigitalOcean cloud credential is stored for the `appscode`
+> owner.
+
+### GET /clouds/{owner}/providers/digitalocean/clusters/{id}
+
+Returns detailed information for a single DigitalOcean cluster (for import).
+
+- **Path parameters:** `id` (string) — DigitalOcean cluster id.
+- **Response** `200` — a [`ClusterInfo`](#common-response-shapes). Errors: `404`,
+ `409` (import conflict).
+
+> **Verified:** requires stored DigitalOcean credentials; not reachable for
+> `appscode`.
+
+---
+
+## Linode discovery
+
+Linode Kubernetes Engine (LKE) discovery routes. All are `GET`,
+token-authenticated, and require stored Linode credentials for `{owner}`.
+
+### GET /clouds/{owner}/providers/linode/clusters
+
+Lists the Linode Kubernetes Engine (LKE) clusters.
+
+- **Response** `200` — an array of [`ClusterInfo`](#common-response-shapes).
+
+> **Verified:** returned `302` (redirect to `/500`) against `appscode` on
+> 2026-07-14 — no Linode cloud credential is stored for the `appscode` owner.
+
+### GET /clouds/{owner}/providers/linode/clusters/{id}
+
+Returns detailed information for a single Linode cluster (for import).
+
+- **Path parameters:** `id` (integer, int64) — numeric LKE cluster id.
+- **Response** `200` — a [`ClusterInfo`](#common-response-shapes). Errors: `404`,
+ `409` (import conflict).
+
+> **Verified:** requires stored Linode credentials; not reachable for `appscode`.
+
+---
+
+## Rancher discovery
+
+Rancher-managed cluster discovery routes. All are `GET`, token-authenticated, and
+require both stored cloud credentials and a configured Rancher client for
+`{owner}`.
+
+### GET /clouds/{owner}/providers/rancher/clusters/
+
+Lists the clusters managed by the configured Rancher server.
+
+- **Response** `200` — an array of [`ClusterInfo`](#common-response-shapes).
+
+> **Verified:** requires stored credentials and a configured Rancher client; not
+> reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/rancher/clusters/{id}
+
+Returns detailed information for a single Rancher-managed cluster (for import).
+
+- **Path parameters:** `id` (string) — Rancher cluster id.
+- **Response** `200` — a [`ClusterInfo`](#common-response-shapes). Errors: `404`,
+ `409` (import conflict).
+
+> **Verified:** requires stored credentials and a configured Rancher client; not
+> reachable for `appscode`.
+
+---
+
+## Hetzner discovery
+
+Hetzner Cloud discovery routes. All are `GET`, token-authenticated, and require
+stored Hetzner credentials for `{owner}`.
+
+### GET /clouds/{owner}/providers/hetzner/servers
+
+Lists all Hetzner Cloud server types (machine types).
+
+- **Response** `200` — an array of [`VMInfo`](#common-response-shapes).
+
+> **Verified:** returned `302` (redirect to `/500`) against `appscode` on
+> 2026-07-14 — no Hetzner cloud credential is stored for the `appscode` owner.
+
+### GET /clouds/{owner}/providers/hetzner/kubernetesversions
+
+Lists the supported Kubernetes versions for Hetzner.
+
+- **Response** `200` — an array of version strings.
+
+> **Verified:** returned `302` (redirect to `/500`) against `appscode` on
+> 2026-07-14 — the handler resolves the Hetzner credential before returning
+> versions, and none is stored for `appscode`.
+
+### GET /clouds/{owner}/providers/hetzner/regions/
+
+Lists the available Hetzner Cloud regions (locations).
+
+- **Response** `200` — an array of [`Locations`](#common-response-shapes).
+
+> **Verified:** requires stored Hetzner credentials; not reachable for `appscode`.
+
+### GET /clouds/{owner}/providers/hetzner/regions/{region}/servers
+
+Lists the Hetzner Cloud server types available in the given region.
+
+- **Path parameters:** `region` (string) — Hetzner region (location).
+- **Response** `200` — an array of [`VMInfo`](#common-response-shapes).
+
+> **Verified:** requires stored Hetzner credentials; not reachable for `appscode`.
+
+---
+
+## KubeVirt discovery
+
+### GET /clouds/{owner}/providers/kubevirt/kubernetesversions
+
+Lists the supported Kubernetes versions for KubeVirt-based clusters.
+
+- **Auth:** token.
+- **Response** `200` — an array of version strings:
+
+```json
+["1.27.14", "1.28.10", "1.29.5", "1.30.1"]
+```
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14 (this list
+> is served from static configuration and does not need a cloud credential); the
+> response shown above is the sanitized live payload.
diff --git a/docs/platform/api/cluster-management-v1/_index.md b/docs/platform/api/cluster-management-v1/_index.md
new file mode 100644
index 0000000..2b8d36b
--- /dev/null
+++ b/docs/platform/api/cluster-management-v1/_index.md
@@ -0,0 +1,44 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-cluster-management-v1
+ name: Cluster Management v1
+ parent: api
+ weight: 50
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Cluster Management v1
+
+The core cluster API. Every endpoint is scoped to a single cluster identified by
+`{owner}` (the organization slug or username that owns the cluster) and `{cluster}`
+(the cluster name), and is served under:
+
+```
+/api/v1/clusters/{owner}/{cluster}/...
+```
+
+All routes require a personal access token sent as
+`Authorization: token ` (it may also be supplied as a `token` or
+`access_token` query parameter). Cluster routes additionally resolve the cluster's
+stored credentials and build a Kubernetes client for it ("cluster assignment"); a
+few routes layer on extra authorization checks, which are called out per endpoint.
+
+This group covers three related surfaces:
+
+## Pages
+
+- [Cluster Lifecycle & Info](../lifecycle.md) — get, update, and
+ delete a cluster; fetch its kubeconfig; discover available resource types; database
+ status/bundle; feature values; execute whitelisted commands; create resources;
+ install/uninstall deploy orders; and read resource history.
+- [Kubernetes Proxy](../kubernetes-proxy.md) — the generic
+ `/proxy/*` passthrough for any Kubernetes group/version/resource (CRUD, status,
+ events, controller, scale, HPAs), plus pod logs/exec, node/pod metrics, access
+ reviews, the `meta.k8s.appscode.com` render endpoints, scanner/policy reports, and
+ batch delete.
+- [Helm](../helm.md) — Helm console (tiller) configuration, Helm
+ v3 release management (list/install/upgrade/uninstall/rollback, content, status,
+ history), bundle/package views, and the resource editor.
diff --git a/docs/platform/api/cluster-management-v1/helm.md b/docs/platform/api/cluster-management-v1/helm.md
new file mode 100644
index 0000000..267d8a4
--- /dev/null
+++ b/docs/platform/api/cluster-management-v1/helm.md
@@ -0,0 +1,407 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-cluster-management-v1-helm
+ name: Helm
+ parent: api-cluster-management-v1
+ weight: 30
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Helm
+
+Manage Helm on a member cluster: the Helm console (tiller) configuration, Helm v3
+release lifecycle (list/install/upgrade/uninstall/rollback, plus content, status, and
+history), chart bundle/package views, and the resource editor used to generate and
+apply installations.
+
+All paths on this page are relative to:
+
+```
+/api/v1/clusters/{owner}/{cluster}
+```
+
+All endpoints require a token (`Authorization: token `). Common path
+parameters used throughout this page:
+
+| Name | Type | Description |
+|---|---|---|
+| `owner` | string | Organization slug or username that owns the cluster. |
+| `cluster` | string | Cluster name. |
+| `name` | string | Helm release name (on release endpoints). |
+
+Several install/upgrade/uninstall/rollback/editor operations are submitted as async
+tasks and return a `TaskResponse` (`{ "id": "...", "subject": "..." }`) rather than
+completing synchronously. Many request/response bodies are opaque Kubernetes-style or
+Helm library objects, passed through verbatim; those are noted per endpoint.
+
+A documented call looks like:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/helm/v3/releases/
+```
+
+---
+
+## Console (tiller) configuration
+
+### GET /clusters/{owner}/{cluster}/helm/tiller-config
+
+Get the Helm console (tiller/driver) config.
+
+- **Auth:** token.
+- **Response:** `200` with a `ConsoleConfig`.
+
+```json
+{
+ "id": 1,
+ "userID": 2,
+ "clusterID": "",
+ "type": 1,
+ "data": { "driver": { "helm3": { "driverName": "Secret" } } },
+ "createdUnix": 1783967505,
+ "updatedUnix": 1783967505
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `id` | integer | Config record ID. |
+| `userID` | integer | Owning user ID. |
+| `clusterID` | string | Cluster UID. |
+| `type` | string/int | Setting type (`models.SettingType`). |
+| `data` | object | Config payload (e.g. the Helm driver options). |
+| `createdUnix` / `updatedUnix` | integer | Unix create/update timestamps. |
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) and
+> `appscode/arnob-dev` (spoke) on 2026-07-14.
+
+### PUT /clusters/{owner}/{cluster}/helm/tiller-config
+
+Update the Helm console (tiller/driver) config.
+
+- **Auth:** token.
+- **Request body:** a `TillerOptions` object.
+
+```json
+{ "driver": { "helm3": { "driverName": "Secret" } } }
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `driver.helm3.driverName` | string | no | Helm 3 storage driver (e.g. `Secret`, `ConfigMap`). |
+
+- **Response:** `200` with the updated `ConsoleConfig`.
+
+### GET /clusters/{owner}/{cluster}/helm/tiller-config/default
+
+Get the default Helm console (tiller) config.
+
+- **Auth:** token.
+- **Response:** `200` with a `ConsoleConfig` (same shape as above).
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+---
+
+## Helm v3 releases
+
+### GET /clusters/{owner}/{cluster}/helm/v3/releases/
+
+List Helm v3 releases in the cluster.
+
+- **Auth:** token.
+- **Request body:** optional Helm list options (`action.ListOptions`), passed as an
+ opaque object.
+- **Response:** `200` with an array of Helm `release.Release` objects.
+
+```json
+[
+ {
+ "name": "ace",
+ "info": {
+ "first_deployed": "2026-07-05T06:42:46Z",
+ "last_deployed": "2026-07-05T06:49:03Z",
+ "status": "deployed",
+ "description": "Upgrade complete"
+ },
+ "chart": { "metadata": { "name": "ace" } }
+ }
+]
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) and
+> `appscode/arnob-dev` (spoke) on 2026-07-14.
+
+### POST /clusters/{owner}/{cluster}/helm/v3/releases/
+
+Install a Helm v3 release. Submitted as an async task.
+
+- **Auth:** token.
+- **Request body:** Helm install options (`action.InstallOptions`), passed as an
+ opaque object.
+- **Response:** `200` with a `TaskResponse`.
+
+```json
+{ "id": "task-9f8e7d", "subject": "tasks.helm.install" }
+```
+
+### GET /clusters/{owner}/{cluster}/helm/v3/releases/{name}/
+
+Get the revision history of a Helm release.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `name` (release name).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `max` | integer | no | Maximum number of history revisions to return. |
+
+- **Response:** `200` with an array of Helm `release.Release` objects (one per
+ revision); `404` if the release is not found.
+
+### PUT /clusters/{owner}/{cluster}/helm/v3/releases/{name}/
+
+Upgrade a Helm v3 release. Submitted as an async task.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `name` (release name).
+- **Request body:** Helm upgrade options (`action.UpgradeOptions`), passed as an
+ opaque object.
+- **Response:** `200` with a `TaskResponse`; `404` if the release is not found.
+
+### DELETE /clusters/{owner}/{cluster}/helm/v3/releases/{name}/
+
+Uninstall a Helm v3 release. Submitted as an async task.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `name` (release name).
+- **Request body:** an `UninstallReleaseRequest`.
+
+```json
+{
+ "namespace": "default",
+ "disable_hooks": false,
+ "purge": true,
+ "timeout": 300
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `namespace` | string | no | Namespace of the release. |
+| `disable_hooks` | boolean | no | Skip running lifecycle hooks. |
+| `purge` | boolean | no | Remove the release history. |
+| `timeout` | integer (int64) | no | Operation timeout (seconds). |
+
+- **Response:** `200` with a `TaskResponse`; `404` if the release is not found.
+
+### GET /clusters/{owner}/{cluster}/helm/v3/releases/{name}/content
+
+Get the content (manifest and metadata) of a Helm release.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `name` (release name).
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `version` | integer | no | Specific release revision. |
+| `namespace` | string | no | Namespace of the release. |
+
+- **Response:** `200` with the Helm `release.Release` object.
+
+### GET /clusters/{owner}/{cluster}/helm/v3/releases/{name}/status
+
+Get the status of a Helm release.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `name` (release name).
+- **Query parameters:** `version` (integer, optional revision) and `namespace`
+ (string, optional).
+- **Response:** `200` with the Helm `release.Release` object.
+
+### POST /clusters/{owner}/{cluster}/helm/v3/releases/{name}/rollback
+
+Roll back a Helm v3 release to a previous revision. Submitted as an async task.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `name` (release name).
+- **Request body:** a `RollbackReleaseRequest`.
+
+```json
+{
+ "namespace": "default",
+ "version": 2,
+ "dry_run": false,
+ "recreate": false,
+ "wait": true,
+ "force": false,
+ "timeout": 300,
+ "description": "rollback to v2"
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `namespace` | string | no | Namespace of the release. |
+| `version` | integer (int32) | no | Target revision to roll back to. |
+| `dry_run` | boolean | no | Simulate the rollback. |
+| `disable_hooks` | boolean | no | Skip lifecycle hooks. |
+| `recreate` | boolean | no | Recreate pods. |
+| `wait` | boolean | no | Wait until resources are ready. |
+| `force` | boolean | no | Force resource updates. |
+| `cleanup_on_fail` | boolean | no | Delete new resources on failure. |
+| `timeout` | integer (int64) | no | Operation timeout (seconds). |
+| `description` | string | no | Rollback description. |
+
+- **Response:** `200` with a `TaskResponse`; `404` if the release is not found.
+
+---
+
+## Bundle & package views
+
+These endpoints let you inspect a chart before installing it. The bundle/package
+request and response bodies are `releasesapi`/`chartsapi` objects passed through as
+opaque objects.
+
+### GET /clusters/{owner}/{cluster}/helm/bundleview
+
+Get the bundle view for a chart.
+
+- **Auth:** token.
+- **Request body:** a chart source reference (`releasesapi.ChartSourceFlatRef`).
+- **Response:** `200` with the bundle view (`releasesapi.BundleView`).
+
+### POST /clusters/{owner}/{cluster}/helm/bundleview/orders
+
+Create an order for a bundle view.
+
+- **Auth:** token.
+- **Request body:** a bundle view (`releasesapi.BundleView`).
+- **Response:** `200` with the generated order (`releasesapi.Order`).
+
+### GET /clusters/{owner}/{cluster}/helm/packageview
+
+Get the package view for a chart.
+
+- **Auth:** token.
+- **Request body:** a chart source reference (`releasesapi.ChartSourceFlatRef`).
+- **Response:** `200` with the package view (`releasesapi.PackageView`).
+
+### GET /clusters/{owner}/{cluster}/helm/packageview/files
+
+List the files inside a chart package.
+
+- **Auth:** token.
+- **Request body:** a chart source reference (`releasesapi.ChartSourceFlatRef`).
+- **Response:** `200` with an array of file-path strings.
+
+```json
+["Chart.yaml", "values.yaml", "templates/deployment.yaml"]
+```
+
+### GET /clusters/{owner}/{cluster}/helm/packageview/files/{path}
+
+Get a single file from a chart package.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `path` (the file path within the chart
+ package).
+- **Request body:** a chart source reference (`releasesapi.ChartSourceFlatRef`).
+- **Response:** `200` with the raw file content (`application/octet-stream`;
+ content-type varies); `404` if the file is not found.
+
+### POST /clusters/{owner}/{cluster}/helm/packageview/orders
+
+Create an order for an editor package view / chart.
+
+- **Auth:** token.
+- **Request body:** a chart order (`releasesapi.ChartOrder`).
+- **Response:** `200` with the generated order (`releasesapi.Order`).
+
+### GET /clusters/{owner}/{cluster}/helm/packageview/values
+
+Get the merged values file for a chart preset.
+
+- **Auth:** token.
+- **Request body:** a chart preset reference (`chartsapi.ChartPresetFlatRef`).
+- **Response:** `200` with the merged chart values (a dynamic map).
+
+---
+
+## Resource editor
+
+The editor generates an editor model from options, previews the resulting manifest
+and resources, applies (installs/updates) or deletes an installation, and loads state
+back from an existing install. The `options` bodies are arbitrary JSON objects; the
+load/apply bodies are `releasesapi` objects passed through opaquely.
+
+### PUT /clusters/{owner}/{cluster}/helm/options/model
+
+Generate an editor model from options.
+
+- **Auth:** token.
+- **Request body:** an arbitrary JSON options object.
+- **Response:** `200` with the generated editor model (unstructured object; YAML when
+ requested).
+
+### PUT /clusters/{owner}/{cluster}/helm/options/manifest
+
+Preview the editor manifest from options.
+
+- **Auth:** token.
+- **Request body:** an arbitrary JSON options object.
+- **Response:** `200` with the rendered manifest as raw `text/plain` YAML.
+
+### PUT /clusters/{owner}/{cluster}/helm/options/resources
+
+Preview the editor resources from options.
+
+- **Auth:** token.
+- **Request body:** an arbitrary JSON options object.
+- **Response:** `200` with the rendered resources (`releasesapi.ResourceOutput`).
+
+### PUT /clusters/{owner}/{cluster}/helm/editor/
+
+Apply (create/update/install) a resource editor model. Submitted as an async task.
+
+- **Auth:** token.
+- **Request body:** an arbitrary JSON editor model object.
+- **Response:** `200` with a `TaskResponse`.
+
+### DELETE /clusters/{owner}/{cluster}/helm/editor/
+
+Delete a resource editor installation. Submitted as an async task.
+
+- **Auth:** token.
+- **Request body:** resource metadata (`releasesapi.MetadataFlat`).
+- **Response:** `200` with a `TaskResponse`.
+
+### PUT /clusters/{owner}/{cluster}/helm/editor/model
+
+Load an editor model from an existing installation.
+
+- **Auth:** token.
+- **Request body:** an editor model (`releasesapi.Model`).
+- **Response:** `200` with the editor model values (raw JSON/YAML).
+
+### PUT /clusters/{owner}/{cluster}/helm/editor/manifest
+
+Load the editor manifest from an existing installation.
+
+- **Auth:** token.
+- **Request body:** an editor model (`releasesapi.Model`).
+- **Response:** `200` with the rendered manifest as raw `text/plain` YAML.
+
+### PUT /clusters/{owner}/{cluster}/helm/editor/resources
+
+Load the editor resources from an existing installation.
+
+- **Auth:** token.
+- **Request body:** editor model metadata (`releasesapi.ModelMetadata`).
+- **Response:** `200` with the rendered resources (`releasesapi.ResourceOutput`).
diff --git a/docs/platform/api/cluster-management-v1/kubernetes-proxy.md b/docs/platform/api/cluster-management-v1/kubernetes-proxy.md
new file mode 100644
index 0000000..53930f1
--- /dev/null
+++ b/docs/platform/api/cluster-management-v1/kubernetes-proxy.md
@@ -0,0 +1,533 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-cluster-management-v1-kubernetes-proxy
+ name: Kubernetes Proxy
+ parent: api-cluster-management-v1
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Kubernetes Proxy
+
+A generic passthrough to a member cluster's Kubernetes API, powering the ACE
+console's resource pages. You can perform CRUD on any group/version/resource (GVR),
+read subresources (status, scale, events, controller), stream pod logs, exec into
+pods, read node/pod metrics, run access reviews, drive the `meta.k8s.appscode.com`
+render/report endpoints, and batch-delete resources.
+
+All paths on this page are relative to:
+
+```
+/api/v1/clusters/{owner}/{cluster}
+```
+
+All endpoints require a token (`Authorization: token `). Common path
+parameters used throughout this page:
+
+| Name | Type | Description |
+|---|---|---|
+| `owner` | string | Organization slug or username that owns the cluster. |
+| `cluster` | string | Cluster name. |
+| `group` | string | Kubernetes API group. Use `core` for the legacy core group. |
+| `version` | string | Kubernetes API version (e.g. `v1`). |
+| `resource` | string | Kubernetes resource (plural, e.g. `deployments`). |
+| `namespace` | string | Namespace (for namespaced paths). |
+| `name` | string | Object name. |
+
+Request and response bodies are arbitrary Kubernetes objects (`K8sObject`) —
+free-form objects with an arbitrary group/version/kind, passed to and from the member
+cluster verbatim. List responses are Kubernetes list objects; when `convertToTable`
+is set they are returned as a `Table`. Most list/get endpoints support standard
+Kubernetes label/field selectors as query parameters, plus a `filter` query for
+content filtering and `convertToTable` for table output.
+
+A documented list call looks like:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/proxy/apps/v1/namespaces/kube-system/deployments/
+```
+
+---
+
+## Cluster-scoped resources
+
+### POST /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/
+
+Create a cluster-scoped Kubernetes object of the given GVR.
+
+- **Auth:** token.
+- **Request body:** an arbitrary Kubernetes object.
+- **Response:** `200` with the created object.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/
+
+List cluster-scoped resources of the given GVR. Supports label/field selectors,
+`filter`, and `convertToTable`.
+
+- **Auth:** token.
+- **Response:** `200` with a Kubernetes list (or a `Table`); `304` when the ETag
+ matches.
+
+```json
+{
+ "apiVersion": "v1",
+ "kind": "NodeList",
+ "items": [
+ { "apiVersion": "v1", "kind": "Node", "metadata": { "name": "node-1" } }
+ ]
+}
+```
+
+> **Verified:** `GET .../proxy/core/v1/nodes/` returned `200` against `appscode/ace`
+> (hub) on 2026-07-14.
+
+### DELETE /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/
+
+Delete a collection of cluster-scoped resources. The optional request body carries a
+`metav1.DeleteOptions` object.
+
+- **Auth:** token.
+- **Response:** `200` on success.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/{name}
+
+Get a single cluster-scoped resource by name.
+
+- **Auth:** token.
+- **Response:** `200` with the object (or a `Table`); `304` when the ETag matches;
+ `404` if not found.
+
+### PUT /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/{name}
+
+Replace a cluster-scoped resource.
+
+- **Auth:** token.
+- **Request body:** the full replacement Kubernetes object.
+- **Response:** `200` with the updated object.
+
+### PATCH /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/{name}
+
+Patch a cluster-scoped resource. Supports JSON/merge/strategic-merge patch types via
+the `Content-Type` header; a plain JSON body is diffed against the current object to
+build a JSON patch.
+
+- **Auth:** token.
+- **Request body:** the patch (or a desired object to diff).
+- **Response:** `200` with the patched object.
+
+### DELETE /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/{name}
+
+Delete a cluster-scoped resource by name. The optional request body carries
+`metav1.DeleteOptions`.
+
+- **Auth:** token.
+- **Response:** `200` on success.
+
+### PUT · PATCH /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/{name}/status
+
+Update (PUT) or patch (PATCH) the `status` subresource of a cluster-scoped object.
+
+- **Auth:** token.
+- **Request body:** the object (or patch) carrying the new status.
+- **Response:** `200` with the object with updated status.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/{name}/events
+
+List Kubernetes events referencing the named cluster-scoped object.
+
+- **Auth:** token.
+- **Response:** `200` with an event list.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/{resource}/{name}/controller
+
+Get the controlling owner object of the named cluster-scoped object.
+
+- **Auth:** token.
+- **Response:** `200` with the controller object; `404` if none.
+
+---
+
+## Namespaced resources
+
+These mirror the cluster-scoped operations under a `namespaces/{namespace}` segment.
+
+### POST /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/
+
+Create a namespaced resource of the given GVR.
+
+- **Auth:** token.
+- **Request body:** an arbitrary Kubernetes object.
+- **Response:** `200` with the created object.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/
+
+List namespaced resources. Supports label/field selectors, `filter`, and
+`convertToTable`.
+
+- **Auth:** token.
+- **Response:** `200` with a Kubernetes list (or a `Table`); `304` when the ETag
+ matches.
+
+```json
+{
+ "apiVersion": "apps/v1",
+ "kind": "DeploymentList",
+ "items": [
+ {
+ "apiVersion": "apps/v1",
+ "kind": "Deployment",
+ "metadata": { "name": "coredns", "namespace": "kube-system" }
+ }
+ ]
+}
+```
+
+> **Verified:** `GET .../proxy/apps/v1/namespaces/kube-system/deployments/` returned
+> `200` against `appscode/ace` (hub) and `appscode/arnob-dev` (spoke) on 2026-07-14.
+
+### DELETE /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/
+
+Delete a collection of namespaced resources. Optional body carries
+`metav1.DeleteOptions`.
+
+- **Auth:** token.
+- **Response:** `200` on success.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}
+
+Get a namespaced resource by name.
+
+- **Auth:** token.
+- **Response:** `200` with the object (or a `Table`); `304` when the ETag matches;
+ `404` if not found.
+
+> **Verified:** `GET .../deployments/coredns` returned `200` against `appscode/ace`
+> (hub) on 2026-07-14.
+
+### PUT /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}
+
+Replace a namespaced resource.
+
+- **Auth:** token.
+- **Request body:** the full replacement Kubernetes object.
+- **Response:** `200` with the updated object.
+
+### PATCH /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}
+
+Patch a namespaced resource (JSON/merge/strategic-merge, or a plain JSON body diffed
+against the current object).
+
+- **Auth:** token.
+- **Response:** `200` with the patched object.
+
+### DELETE /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}
+
+Delete a namespaced resource by name. Optional body carries `metav1.DeleteOptions`.
+
+- **Auth:** token.
+- **Response:** `200` on success.
+
+### PUT · PATCH /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}/status
+
+Update (PUT) or patch (PATCH) the `status` subresource of a namespaced object.
+
+- **Auth:** token.
+- **Response:** `200` with the object with updated status.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}/events
+
+List Kubernetes events referencing the named namespaced object.
+
+- **Auth:** token.
+- **Response:** `200` with an event list.
+
+> **Verified:** `GET .../deployments/coredns/events` returned `200` against
+> `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}/controller
+
+Get the controlling owner object of the named namespaced object.
+
+- **Auth:** token.
+- **Response:** `200` with the controller object; `404` if none.
+
+---
+
+## Scale & autoscaling
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}/scale
+
+Read the `scale` subresource of a namespaced object.
+
+- **Auth:** token.
+- **Response:** `200` with the scale object.
+
+> **Verified:** `GET .../deployments/coredns/scale` returned `200` against
+> `appscode/ace` (hub) on 2026-07-14.
+
+### PUT /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}/scale
+
+Update the replica count of a namespaced object via its `scale` subresource.
+
+- **Auth:** token.
+- **Request body:**
+
+```json
+{ "replicas": 3 }
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `replicas` | integer (int32) | yes | Desired replica count. |
+
+- **Response:** `200` with the updated scale object.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/namespaces/{namespace}/{resource}/{name}/horizontalpodautoscalers
+
+List the HorizontalPodAutoscalers whose scale target matches the named namespaced
+object.
+
+- **Auth:** token.
+- **Response:** `200` with a filtered HPA list.
+
+---
+
+## Pods: logs, exec & metrics
+
+### GET /clusters/{owner}/{cluster}/proxy/core/v1/namespaces/{namespace}/{resource}/{name}/log
+
+Stream container logs for the named pod. The response is a streaming plain-text body.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, `namespace`, `resource` (`pods`), `name`.
+- **Response:** `200` streaming `text/plain` log output; `404` if the pod is not
+ found.
+
+### GET /clusters/{owner}/{cluster}/proxy/core/v1/namespaces/{namespace}/{resource}/{name}/exec
+
+Establish an interactive exec session into a container of the named pod. This
+endpoint is upgraded to a **WebSocket** connection — it is not a plain HTTP
+request/response.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, `namespace`, `resource` (`pods`), `name`.
+- **Response:** `101` Switching Protocols (WebSocket exec stream); `404` if the pod is
+ not found.
+
+### GET /clusters/{owner}/{cluster}/proxy/core/v1/nodes/{name}/metrics
+
+Get resource-usage metrics for the named node.
+
+- **Auth:** token.
+- **Response:** `200` with a metrics object; `404` if the node is not found.
+
+### GET /clusters/{owner}/{cluster}/proxy/core/v1/namespaces/{namespace}/pods/{name}/metrics
+
+Get resource-usage metrics for the named pod.
+
+- **Auth:** token.
+- **Response:** `200` with a metrics object; `404` if the pod is not found.
+
+---
+
+## Access reviews & discovery
+
+### POST /clusters/{owner}/{cluster}/proxy/authorization.k8s.io/v1/selfsubjectaccessreviews
+
+Submit a `SelfSubjectAccessReview` to check whether the current user can perform an
+action ("can I" check).
+
+- **Auth:** token.
+- **Request body:** a `SelfSubjectAccessReview` Kubernetes object.
+
+```json
+{
+ "apiVersion": "authorization.k8s.io/v1",
+ "kind": "SelfSubjectAccessReview",
+ "spec": {
+ "resourceAttributes": {
+ "namespace": "default",
+ "verb": "list",
+ "resource": "pods"
+ }
+ }
+}
+```
+
+- **Response:** `200` with the review and its populated `status`.
+
+### GET /clusters/{owner}/{cluster}/proxy/{group}/{version}/all-available
+
+Discover all resources under the given API group/version and return, per Kind, the
+list of object names available in the cluster.
+
+- **Auth:** token.
+- **Response:** `200` with a map of Kind to an array of object names.
+
+```json
+{
+ "Deployment": ["coredns", "metrics-server"],
+ "StatefulSet": ["prometheus"]
+}
+```
+
+> **Verified:** returned `404` against `appscode/ace` for `apps/v1` and `core/v1` —
+> this discovery endpoint was not available/enabled on the verification cluster.
+
+---
+
+## Render endpoints (meta.k8s.appscode.com)
+
+These endpoints render `meta.k8s.appscode.com/v1alpha1` UI objects. They are
+implemented as **cacheable creates**: the request object is passed JSON-encoded via
+the `q` query parameter so the response can be cached (supporting `304 Not Modified`
+via ETags).
+
+Common query parameter:
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `q` | string | yes | JSON-encoded request object for the render. |
+
+### GET /clusters/{owner}/{cluster}/proxy/meta.k8s.appscode.com/v1alpha1/renders
+
+Render a `Render` object (resource layout).
+
+- **Auth:** token.
+- **Response:** `200` with the rendered object; `304` when the ETag matches.
+
+### GET /clusters/{owner}/{cluster}/proxy/meta.k8s.appscode.com/v1alpha1/renderdashboards
+
+Render a `RenderDashboard` object.
+
+- **Auth:** token.
+- **Response:** `200` with the rendered dashboards object; `304` when the ETag
+ matches.
+
+### GET /clusters/{owner}/{cluster}/proxy/meta.k8s.appscode.com/v1alpha1/rendermenus
+
+Render a `RenderMenu` object.
+
+- **Auth:** token.
+- **Response:** `200` with the rendered menus object; `304` when the ETag matches.
+
+> **Verified:** returned `400` against `appscode/ace` when passed a minimal `q`
+> object — the endpoint is reachable but requires a fully-formed `RenderMenu` request
+> object; only GET was exercised.
+
+### GET /clusters/{owner}/{cluster}/proxy/meta.k8s.appscode.com/v1alpha1/resourcegraphs
+
+Render a `ResourceGraph` object.
+
+- **Auth:** token.
+- **Response:** `200` with the rendered resource-graph object; `304` when the ETag
+ matches.
+
+### GET /clusters/{owner}/{cluster}/proxy/meta.k8s.appscode.com/v1alpha1/{resource}/{menu}/available
+
+Get the available user menu entries for the given `meta.k8s.appscode.com` resource and
+menu.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `resource` (the
+ `meta.k8s.appscode.com` resource name) and `menu` (menu identifier).
+- **Response:** `200` with an available-user-menus object.
+
+---
+
+## Scanner & policy reports
+
+Like the render endpoints, these are cacheable creates driven by a JSON-encoded `q`
+query parameter.
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `q` | string | yes | JSON-encoded report request object. |
+
+### GET /clusters/{owner}/{cluster}/proxy/reports.scanner.appscode.com/v1alpha1/images
+
+Produce a `reports.scanner.appscode.com` `Image` report.
+
+- **Auth:** token.
+- **Response:** `200` with the image report object; `304` when the ETag matches.
+
+> **Verified:** returned `500` against `appscode/ace` when the `q` query parameter is
+> omitted — the endpoint requires a JSON-encoded `Image` report request object; only
+> GET was exercised.
+
+### GET /clusters/{owner}/{cluster}/proxy/reports.scanner.appscode.com/v1alpha1/cvereports
+
+Produce a `reports.scanner.appscode.com` `CVEReport`.
+
+- **Auth:** token.
+- **Response:** `200` with the CVE report object; `304` when the ETag matches.
+
+### GET /clusters/{owner}/{cluster}/proxy/policy.k8s.appscode.com/v1alpha1/policyreports
+
+Produce a `policy.k8s.appscode.com` `PolicyReport`.
+
+- **Auth:** token.
+- **Response:** `200` with the policy report object; `304` when the ETag matches.
+
+> **Verified:** returned `500` against `appscode/ace` when the `q` query parameter is
+> omitted — the endpoint requires a JSON-encoded `PolicyReport` request object; only
+> GET was exercised.
+
+---
+
+## Batch delete
+
+### POST /clusters/{owner}/{cluster}/proxy/batch-delete
+
+Delete multiple Kubernetes objects in a single request. Each entry is deleted
+independently; the response reports the per-resource deletion status for any that
+failed.
+
+- **Auth:** token.
+- **Request body:** a `BatchDeleteRequest`.
+
+```json
+{
+ "resources": [
+ {
+ "namespace": "default",
+ "name": "example",
+ "group": "",
+ "version": "v1",
+ "resource": "configmaps"
+ }
+ ]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `resources` | array | yes | The resources to delete. |
+| `resources[].namespace` | string | no | Namespace (empty for cluster-scoped resources). |
+| `resources[].name` | string | no | Object name. |
+| `resources[].group` | string | no | Kubernetes API group. |
+| `resources[].version` | string | no | Kubernetes API version. |
+| `resources[].resource` | string | no | Kubernetes resource (plural). |
+
+- **Response:** `200` with a `BatchDeleteResponse`. The `resourcesStatus` array is
+ populated for resources whose deletion failed, each carrying its identifiers plus a
+ `metav1.Status` describing the result.
+
+```json
+{
+ "resourcesStatus": [
+ {
+ "namespace": "default",
+ "name": "example",
+ "group": "",
+ "version": "v1",
+ "resource": "configmaps",
+ "status": { "kind": "Status", "status": "Failure", "message": "not found" }
+ }
+ ]
+}
+```
diff --git a/docs/platform/api/cluster-management-v1/lifecycle.md b/docs/platform/api/cluster-management-v1/lifecycle.md
new file mode 100644
index 0000000..421f31a
--- /dev/null
+++ b/docs/platform/api/cluster-management-v1/lifecycle.md
@@ -0,0 +1,361 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-cluster-management-v1-lifecycle
+ name: Cluster Lifecycle & Info
+ parent: api-cluster-management-v1
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Cluster Lifecycle & Info
+
+Manage the lifecycle of an imported cluster and read information about it. All paths
+on this page are relative to:
+
+```
+/api/v1/clusters/{owner}/{cluster}
+```
+
+All endpoints require a token (`Authorization: token `). `{owner}` is the
+organization slug or username that owns the cluster, and `{cluster}` is the cluster
+name. Common path parameters used throughout this page:
+
+| Name | Type | Description |
+|---|---|---|
+| `owner` | string | Organization slug or username that owns the cluster. |
+| `cluster` | string | Cluster name. |
+
+A documented call looks like:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/
+```
+
+---
+
+## Cluster details
+
+### GET /clusters/{owner}/{cluster}/
+
+Get summary information about a cluster.
+
+- **Auth:** token.
+- **Response:** `200` with a `ClusterInfo` object.
+
+```json
+{
+ "id": 2,
+ "displayName": "ace",
+ "name": "ace",
+ "uid": "",
+ "ownerID": 3,
+ "externalID": "",
+ "ownerName": "appscode",
+ "provider": "Generic",
+ "vendor": "",
+ "infraNamespace": "",
+ "isMonitoringCluster": false,
+ "endpoint": "https://:6443",
+ "location": "",
+ "project": "",
+ "kubernetesVersion": "v1.36.2+k3s1",
+ "nodeCount": 1,
+ "createdAt": "2026-07-05T06:49:31Z",
+ "age": "8d",
+ "clusterManagers": ["ACE", "OCMHub"],
+ "status": { "response": { "phase": "Active" } }
+}
+```
+
+Key fields: `name`/`displayName`/`uid` identify the cluster, `ownerName`/`ownerID`
+the owning account, `provider`/`vendor`/`kubernetesVersion`/`nodeCount` describe the
+Kubernetes cluster, `clusterManagers` lists the managers (e.g. `ACE`, `OCMHub`,
+`Rancher`), and `status` is a Kubernetes-style `ClusterStatus` object.
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) and `appscode/arnob-dev` (spoke) on 2026-07-14.
+
+### PUT /clusters/{owner}/{cluster}/
+
+Update an imported cluster's import options (display name, provider, kubeconfig).
+
+- **Auth:** token + `authzCheck(update:cluster)` (Cluster_Editor).
+- **Request body:** an `ImportClusterOption`.
+
+```json
+{
+ "displayName": "my-cluster",
+ "name": "my-cluster",
+ "provider": "Generic",
+ "kubeConfig": ""
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `displayName` | string | no | Human-friendly name. |
+| `name` | string | no | Cluster name. |
+| `provider` | string | yes | Cluster provider (e.g. `Generic`, `GKE`, `EKS`). |
+| `kubeConfig` | string | yes | Raw kubeconfig for the cluster. |
+
+- **Response:** `200` with the persisted cluster info model (`ClusterInfoModel`, an
+ open/DB-backed object).
+
+### DELETE /clusters/{owner}/{cluster}/
+
+Submit a cluster deletion command as an async task. Returns the command to run rather
+than deleting synchronously.
+
+- **Auth:** token + `authzCheck(delete:cluster)` (Cluster_Editor).
+- **Response:** `200` with a `TaskResponse`.
+
+```json
+{ "id": "task-1a2b3c", "subject": "tasks.cluster.delete" }
+```
+
+### POST /clusters/{owner}/{cluster}/remove
+
+Remove a cluster's registration from ACE (does not delete the underlying cluster).
+
+- **Auth:** token.
+- **Response:** `200` with an empty body.
+
+---
+
+## Cluster access & discovery
+
+### GET /clusters/{owner}/{cluster}/client-config
+
+Get the cluster's client kubeconfig, returned as a JSON string.
+
+- **Auth:** token + `authzCheck(view:kube-config)` (Cluster_CanViewKubeconfig).
+- **Response:** `200` with the kubeconfig as a JSON-encoded string.
+
+```json
+"apiVersion: v1\nkind: Config\nclusters:\n- cluster: { ... }\n..."
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/available-types
+
+Discover the resource types available in the cluster. Returns a nested mapping of
+`group -> version -> [ { Kind, Resource } ]`.
+
+- **Auth:** token.
+- **Response:** `200` with a dynamic object.
+
+```json
+{
+ "": {
+ "v1": [
+ { "Kind": "ConfigMap", "Resource": "configmaps" },
+ { "Kind": "Pod", "Resource": "pods" },
+ { "Kind": "Endpoints", "Resource": "endpoints" }
+ ]
+ },
+ "apps": {
+ "v1": [
+ { "Kind": "Deployment", "Resource": "deployments" }
+ ]
+ }
+}
+```
+
+The empty-string key (`""`) is the legacy Kubernetes core group.
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) and `appscode/arnob-dev` (spoke) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/is-server
+
+Check whether this cluster is the ACE server (hub) cluster.
+
+- **Auth:** token.
+- **Response:** `200` with `{"server": "yes"|"no"}`.
+
+```json
+{ "server": "yes" }
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub, `"server":"yes"`) on 2026-07-14.
+
+---
+
+## Database & features
+
+### GET /clusters/{owner}/{cluster}/db-status
+
+Get a dynamic map of database feature gates / status for the cluster.
+
+- **Auth:** token.
+- **Response:** `200` with a dynamic map (empty `{}` when no DB features are present).
+
+```json
+{}
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/db-bundle
+
+Get database bundle information — a dynamic map of string to an array of strings.
+
+- **Auth:** token.
+- **Response:** `200` with a dynamic map.
+
+```json
+{
+ "kubedb": ["MongoDB", "PostgreSQL", "MySQL"]
+}
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/feature/factory-values
+
+Get the factory (default) values for one or more features.
+
+- **Auth:** token.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `features` | string | yes | Feature name(s) to fetch default values for. |
+
+- **Response:** `200` with a dynamic map of factory feature values.
+
+> **Verified:** `GET` returned `200` against `appscode/ace` with `?features=kubedb`;
+> returned `400` (`missing features query parameter`) when the `features` query
+> parameter is omitted, on 2026-07-14.
+
+### POST /clusters/{owner}/{cluster}/update-presets/{preset}
+
+Update the named preset in a spoke cluster by applying the supplied FluxCD
+`HelmRelease` object.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `preset` (string, the preset name).
+- **Request body:** an arbitrary FluxCD `HelmRelease` Kubernetes object. See the
+ [Kubernetes proxy](../kubernetes-proxy.md) page for how
+ arbitrary Kubernetes objects are passed through verbatim.
+- **Response:** `200` with an empty body.
+
+---
+
+## Commands & resources
+
+### POST /clusters/{owner}/{cluster}/execute-command
+
+Run one of the whitelisted `kubectl` plugin commands against the cluster
+(`kubectl-kubestash`, `kubectl-stash`, `kubectl-dba`, `kubectl-vault`).
+
+- **Auth:** token.
+- **Request body:** an `ExecuteCommandPayload`.
+
+```json
+{ "command": "kubectl-dba version --client" }
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `command` | string | yes | The whitelisted command to execute. |
+
+- **Response:** `200` with a `CommandResponse`.
+
+```json
+{ "stdout": "kubectl-dba version ...", "stderr": "" }
+```
+
+Returns `422` for an invalid or unsupported command.
+
+### POST /clusters/{owner}/{cluster}/resources
+
+Create a generic Kubernetes resource in the cluster.
+
+- **Auth:** token.
+- **Request body:** the raw YAML/JSON of any Kubernetes object (parsed into an
+ unstructured object). This is an arbitrary Kubernetes object rather than a fixed
+ schema.
+
+```json
+{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": { "name": "example", "namespace": "default" },
+ "data": { "key": "value" }
+}
+```
+
+- **Response:** `200` with the created Kubernetes object.
+
+---
+
+## Deploy orders
+
+### POST /clusters/{owner}/{cluster}/deploy/{id}
+
+Install a previously generated product deploy order (`order.yaml` identified by
+`{id}`) into the cluster. No request body.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `id` (string, the deploy order ID).
+- **Response:** `200` (empty body). `404` if the deploy order is not found.
+
+### DELETE /clusters/{owner}/{cluster}/deploy/{id}
+
+Uninstall the product deploy order identified by `{id}` from the cluster. No request
+body.
+
+- **Auth:** token.
+- **Path parameters:** `owner`, `cluster`, plus `id` (string, the deploy order ID).
+- **Response:** `200` (empty body). `404` if the deploy order is not found.
+
+---
+
+## Resource history
+
+### GET /clusters/{owner}/{cluster}/resource-history
+
+Get the change history for resources from NATS, optionally filtered by query
+parameters.
+
+- **Auth:** token.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `product` | string | no | Filter by product. |
+| `group` | string | no | Filter by Kubernetes API group. |
+| `resource` | string | no | Filter by resource (plural). |
+| `rid` | string | no | Filter by resource ID. |
+
+- **Response:** `200` with a `BadgerEntryList`.
+
+```json
+{
+ "items": [
+ {
+ "key": "kubedb.com/mongodbs/",
+ "values": [
+ {
+ "resourceID": { "group": "kubedb.com", "kind": "MongoDB", "name": "mongo-1" },
+ "licenseID": "",
+ "version": 3,
+ "timestamp": 1783967505
+ }
+ ],
+ "lastSeen": 1783967505
+ }
+ ]
+}
+```
+
+Each entry has a `key`, one or more `values` (each carrying the `resource`,
+`resourceID`, `licenseID`, `version`, and `timestamp`), and a `lastSeen` timestamp.
+
+> **Verified:** returned `500` (`nats: no responders available for request`) against
+> `appscode/ace` — this endpoint depends on the NATS resource-history responder,
+> which was not available on the verification cluster.
diff --git a/docs/platform/api/cluster-management-v2/_index.md b/docs/platform/api/cluster-management-v2/_index.md
new file mode 100644
index 0000000..8e42c7e
--- /dev/null
+++ b/docs/platform/api/cluster-management-v2/_index.md
@@ -0,0 +1,43 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-cluster-management-v2
+ name: Cluster Management v2
+ parent: api
+ weight: 60
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Cluster Management v2
+
+The Cluster Management v2 API is the newer, hub-aware cluster surface served under
+`/api/v1/clustersv2`. It builds on [Cluster Management v1](../../cluster-management-v1/)
+and adds:
+
+- **Hub awareness** — list hub clusters, resolve hub connection info, and check hub
+ presence for an owner (Open Cluster Management, or OCM, integration).
+- **Import & lifecycle** — check/validate/import clusters, connect or re-connect to an
+ already-imported cluster, reconcile Helm releases, reconfigure components, and remove
+ a cluster.
+- **Kubernetes version upgrades** — inspect upgradeable versions and trigger CAPI
+ cluster upgrades.
+- **Gateway configurations** — list gateway configs and update them to the current ACE
+ chart version.
+- **vcluster support** — create or import virtual clusters on a host cluster.
+- **Inbox subscriptions** — subscribe the current user to cluster-, namespace-, or
+ resource-level inbox notifications, plus obtain an inbox agent token for a cluster.
+
+All routes require a personal access token (`Authorization: token `). Most routes are
+scoped to an `owner` (organization slug or username) and, for cluster-scoped routes, a
+`cluster` name; a few (skip-credentials, all-hubs, identity, all-clusters) require
+site-admin privileges.
+
+## Pages
+
+- [Clusters](../clusters.md) — providers, hubs, cluster identity,
+ listing, status, import/connect/remove, reconcile, reconfigure, Kubernetes version
+ upgrades, gateway configs, feature conversion, and vclusters.
+- [Subscriptions & Inbox](../subscriptions.md) — cluster,
+ namespace, and resource notification subscriptions, plus the inbox agent token.
diff --git a/docs/platform/api/cluster-management-v2/clusters.md b/docs/platform/api/cluster-management-v2/clusters.md
new file mode 100644
index 0000000..caea3f0
--- /dev/null
+++ b/docs/platform/api/cluster-management-v2/clusters.md
@@ -0,0 +1,879 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-cluster-management-v2-clusters
+ name: Clusters
+ parent: api-cluster-management-v2
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Clusters (v2)
+
+Cluster lifecycle and hub-aware operations under `/api/v1/clustersv2`. All paths on this
+page are relative to `/api/v1`; the full path for the first endpoint, for example, is
+`/api/v1/clustersv2/providers/{provider}/skip-credentials`.
+
+Common conventions:
+
+- **owner** — the organization slug or username that owns the cluster (e.g. `appscode`).
+- **cluster** — the cluster name within that owner's scope (e.g. `ace`, `arnob-dev`,
+ `arnob-monitoring`).
+- **Auth** — every endpoint requires a token unless noted; send it as
+ `Authorization: token ` (or a `token` / `access_token` query parameter).
+- **K8s object responses** — some endpoints return an arbitrary Kubernetes object or
+ list (for example an `UnstructuredList` of `clusterprofiles`, a `ConfigMapList`, or a
+ cluster `status`). These are passed through verbatim from the member cluster and are
+ documented as opaque objects rather than fixed schemas.
+
+Example request:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clustersv2/appscode/ace/status
+```
+
+---
+
+## Provider & hub discovery
+
+### GET /clustersv2/providers/{provider}/skip-credentials
+
+Check whether manual cloud credentials can be skipped for a provider. Only EKS is
+supported; it verifies the current environment has the AWS IAM permissions needed to skip
+manual credentials.
+
+**Auth:** token. Requires **site-admin** privileges
+(`admin_of_administrative_org:site_admin`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| provider | string | Cloud provider identifier. Only `eks` is supported. |
+
+**Response:** `200` when credentials can be skipped, `400` when they cannot (unsupported
+provider or insufficient permissions). Both return a `SkipCredentialsResponse`:
+
+```json
+{
+ "skipCredentials": true,
+ "reason": "",
+ "message": "IAM permissions verified"
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| skipCredentials | boolean | Whether credentials can be skipped. |
+| reason | string | Machine-readable reason (when not skippable). |
+| message | string | Human-readable detail. |
+
+> **Verified:** `GET` returned `400` for `providers/eks/skip-credentials` on 2026-07-14 — this ACE instance is not an EKS environment with the required IAM permissions, which is the expected non-skippable outcome.
+
+---
+
+### GET /clustersv2/all-hubs
+
+List all hub clusters owned by the user across their organizations.
+
+**Auth:** token.
+
+**Response:** `200` — an array of `HubCluster`. `400` if the user owns no organization.
+
+```json
+[
+ {
+ "name": "ace",
+ "ownerName": "appscode",
+ "displayName": "ace",
+ "hubClusterUID": "",
+ "spokeClusters": 0,
+ "addOns": 0,
+ "clusterNode": 1,
+ "provider": "Generic",
+ "hubType": "OCM",
+ "locations": "",
+ "age": "43m",
+ "createdAt": "2026-07-05T06:49:31Z"
+ }
+]
+```
+
+| Field | Type | Description |
+|---|---|---|
+| name | string | Hub cluster name. |
+| ownerName | string | Owner (org/user) name. |
+| displayName | string | Display name. |
+| hubClusterUID | string | Hub cluster UID. |
+| spokeClusters | integer | Number of spoke clusters. |
+| addOns | integer | Number of add-ons installed. |
+| clusterNode | integer | Node count. |
+| provider | string | Cloud/infra provider. |
+| hubType | string | Hub type (e.g. `OCM`). |
+| locations | string | Locations summary. |
+| age | string | Human-readable age. |
+| createdAt | string (date-time) | Creation timestamp. |
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14 (one hub, `ace`).
+
+---
+
+### GET /clustersv2/{owner}/hub-info
+
+Get hub cluster connection info (API server and token) for the owner's OCM hub clusters.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+
+**Response:** `200` — a map keyed by cluster name; each value contains `apiServer` and a
+bearer `token`. `400` if listing OCM manager clusters fails.
+
+```json
+{
+ "ace": {
+ "apiServer": "https://:6443",
+ "token": ""
+ }
+}
+```
+
+> **Note:** the `token` value is a live service-account bearer token — treat it as a
+> secret.
+
+> **Verified:** `GET` returned `200` against `appscode` (hub `ace`) on 2026-07-14.
+
+---
+
+### GET /clustersv2/{owner}/has-hub
+
+Check whether the owner has any hub cluster.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+
+**Response:** `200` — `{ "has-hub": }`. `400` if listing OCM manager clusters
+fails.
+
+```json
+{ "has-hub": true }
+```
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14 (`{"has-hub":true}`).
+
+---
+
+### GET /clustersv2/cluster-profiles
+
+List available cluster profiles.
+
+**Auth:** token.
+
+**Response:** `200` — an `UnstructuredList` of `clusterprofiles`
+(`meta.k8s.appscode.com/v1alpha1`). This is an arbitrary Kubernetes list object; see the
+[cluster features guide](../../../guides/cluster-management/cluster-features.md) for how
+profiles are used.
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+---
+
+### GET /clustersv2/identity/{clusterID}
+
+Get cluster identity metadata by cluster UID.
+
+**Auth:** token. (Used for cross-cluster identity resolution; intended for
+platform/admin flows.)
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| clusterID | string | Cluster UID. |
+
+**Response:** `200` — a `ClusterMetadata` object. `404` if the cluster info is not found.
+
+```json
+{
+ "uid": "",
+ "name": "ace",
+ "displayName": "ace",
+ "provider": "Generic",
+ "ownerID": "3",
+ "ownerType": "org",
+ "apiEndpoint": "https://:6443",
+ "caBundle": "",
+ "managerID": "",
+ "hubClusterID": "",
+ "cloudServiceAuthMode": "",
+ "mode": "prod"
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| uid | string | Cluster UID. |
+| name | string | Cluster name. |
+| displayName | string | Display name. |
+| provider | string | Provider. |
+| ownerID | string | Owner ID. |
+| ownerType | string | Owner type (org/user). |
+| apiEndpoint | string | Kubernetes API server endpoint. |
+| caBundle | string | Base64-encoded CA bundle. |
+| managerID | string | Managing cluster ID. |
+| hubClusterID | string | Hub cluster UID. |
+| cloudServiceAuthMode | string | Cloud service auth mode. |
+| mode | string | One of `prod`, `qa`, `staging`, `dev`. |
+
+> **Verified:** `GET` returned `404` for the hub cluster UID on 2026-07-14 — the identity
+> registry lookup is keyed differently from the imported-cluster list, so this UID is not
+> resolvable here on this instance (the route itself is reachable and authorized).
+
+---
+
+## Listing & status
+
+### GET /clustersv2/{owner}/all-clusters
+
+List all imported clusters for an owner.
+
+**Auth:** token. Requires **site-admin** privileges (`view_orgs:site_admin`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+
+**Response:** `200` — an array of `ClusterInfo` (see the schema under
+[`GET /clustersv2/{owner}`](#get-clustersv2owner)).
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+---
+
+### GET /clustersv2/{owner}
+
+List clusters for an owner (each with a resolved provider).
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+
+**Response:** `200` — an array of `ClusterInfo`:
+
+```json
+[
+ {
+ "id": 2,
+ "displayName": "ace",
+ "name": "ace",
+ "uid": "",
+ "ownerID": 3,
+ "ownerName": "appscode",
+ "provider": "Generic",
+ "vendor": "",
+ "infraNamespace": "",
+ "isMonitoringCluster": false,
+ "endpoint": "https://:6443",
+ "kubernetesVersion": "v1.36.2+k3s1",
+ "nodeCount": 1,
+ "createdAt": "2026-07-05T06:49:31Z",
+ "age": "43m",
+ "clusterManagers": ["ACE", "OCMHub"],
+ "isCapiClusterUpgradeable": false,
+ "status": { "response": { "phase": "Registered" } }
+ }
+]
+```
+
+| Field | Type | Description |
+|---|---|---|
+| id | integer (int64) | Internal cluster record ID. |
+| displayName | string | Display name. |
+| name | string | Cluster name. |
+| uid | string | Cluster UID. |
+| ownerID | integer (int64) | Owner ID. |
+| managerID | integer (int64) | Managing cluster ID. |
+| externalID | string | External (provider) ID. |
+| hubClusterName / hubClusterUID / hubClusterOwnerName | string | Associated hub cluster. |
+| clusterSetName | string | Cluster-set name. |
+| ownerName | string | Owner (org/user) name. |
+| provider | string | Provider (e.g. `Generic`, `eks`). |
+| vendor | string | Kubernetes vendor. |
+| infraNamespace | string | Infrastructure namespace. |
+| isMonitoringCluster | boolean | Whether this is a monitoring cluster. |
+| endpoint | string | Kubernetes API endpoint. |
+| location / project | string | Location / project. |
+| kubernetesVersion | string | Kubernetes version. |
+| nodeCount | integer (int32) | Node count. |
+| createdAt | string (date-time) | Creation time. |
+| age | string | Human-readable age. |
+| clusterManagers | string[] | Managers (e.g. `ACE`, `OCMHub`). |
+| isCapiClusterUpgradeable | boolean | Whether a CAPI upgrade is available. |
+| status | object | Kubernetes-style `ClusterStatus` (opaque). |
+
+> **Verified:** `GET` returned `200` against `appscode` on 2026-07-14.
+
+---
+
+### GET /clustersv2/{owner}/{cluster}/exist
+
+Check whether the cluster is imported for the given owner.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Response:** `200` — `{ "exist": }`.
+
+```json
+{ "exist": true }
+```
+
+> **Verified:** `GET` returned `200` against `appscode/arnob-dev` on 2026-07-14 (`{"exist":true}`).
+
+---
+
+### GET /clustersv2/{owner}/{cluster}/status
+
+Get the current status of a cluster.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Response:** `200` — a `ClusterInfo` with its `status` field resolved (same schema as
+[`GET /clustersv2/{owner}`](#get-clustersv2owner)).
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14; also
+> `200` against `appscode/arnob-monitoring` (spoke).
+
+---
+
+## Lifecycle & reconciliation
+
+### GET /clustersv2/{owner}/{cluster}/reconcile
+
+Reconcile Helm releases for the specified features. Adds reconcile annotations to the
+`HelmRelease` objects of the features passed in the `features` query parameter.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| features | string | No | Comma-separated list of feature names to reconcile. |
+
+**Response:** `200` when reconcile is triggered. `400` if a feature or its `HelmRelease`
+is not found.
+
+> **Verified:** `GET` returned `500` against `appscode/ace` on 2026-07-14 when called
+> with no `features` argument — the handler requires valid feature names to resolve
+> `HelmRelease` objects.
+
+---
+
+### POST /clustersv2/{owner}/check
+
+Check whether a cluster already exists / is importable.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+
+**Request body:** a `CheckOptions` object.
+
+```json
+{
+ "basicInfo": {
+ "name": "my-cluster",
+ "displayName": "My Cluster",
+ "clusterUID": ""
+ },
+ "provider": {
+ "name": "eks",
+ "credential": "aws-cred",
+ "region": "us-east-1",
+ "clusterID": "my-eks"
+ }
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| basicInfo | object (`BasicInfo`) | No | Cluster basic info (name, displayName, ownerID, managerID, userID, clusterUID, hubClusterID, infraNamespace). |
+| provider | object (`ProviderOptions`) | No | Provider details (name, credential, eksAuthMode `IRSA`/`PodIdentity`, clusterID, project, region, resourceGroup, kubeConfig). |
+
+**Response:** `200` — a `ClusterInfo` describing existence / import status. `422` for an
+invalid body.
+
+---
+
+### POST /clustersv2/{owner}/validate
+
+Validate cluster import options.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| operation | string | No | Set to `connect` to skip Flux/feature-set detection. |
+
+**Request body:** an `ImportOptions` object.
+
+```json
+{
+ "basicInfo": { "name": "my-cluster", "displayName": "My Cluster" },
+ "provider": { "name": "eks", "credential": "aws-cred", "region": "us-east-1" },
+ "components": {
+ "fluxCD": true,
+ "allFeatures": false,
+ "clusterProfile": "default",
+ "featureSets": [ { "name": "opscenter-core", "features": ["kube-ui-server"] } ]
+ }
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| basicInfo | object (`BasicInfo`) | No | Cluster basic info. |
+| provider | object (`ProviderOptions`) | No | Provider details. |
+| components | object (`ComponentOptions`) | No | Components to install: `fluxCD` (bool), `featureSets` (array of `{name, features[]}`), `allFeatures` (bool), `clusterProfile` (string), `monitoringClusterName` (string), `spokeComponent` (bool). |
+
+**Response:** `200` — a `ValidationResponse`:
+
+```json
+{
+ "alreadyImported": false,
+ "displayName": { "valid": true, "reason": "" },
+ "clusterId": { "valid": true, "reason": "" },
+ "message": "",
+ "monitoringClusterList": ["arnob-monitoring"]
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| alreadyImported | boolean | Whether the cluster is already imported. |
+| displayName / clusterId | object (`FieldValidation`) | Per-field `{valid, reason}`. |
+| fluxCD / availableFeatureSets / clusterAPI | object | Opaque Kubernetes objects (detected state). |
+| message | string | Validation message. |
+| clusterMetadata | object (`ClusterMetadata`) | Detected cluster metadata. |
+| monitoringClusterList | string[] | Candidate monitoring clusters. |
+
+`422` for an invalid body.
+
+---
+
+### POST /clustersv2/{owner}/import
+
+Import a cluster.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+
+**Request body:** an `ImportOptions` object (same schema as
+[`/validate`](#post-clustersv2ownervalidate)).
+
+**Response:** `200` — the imported `ClusterInfo`. `422` for an invalid body.
+
+---
+
+### POST /clustersv2/{owner}/{cluster}/connect
+
+Connect (or re-connect) to an already-imported cluster.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| operation | string | No | Set to `re-connect` to update existing credentials. |
+
+**Request body:** a `ConnectOptions` object.
+
+```json
+{
+ "name": "my-cluster",
+ "credential": "aws-cred",
+ "kubeConfig": ""
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| name | string | No | Cluster name. |
+| credential | string | No | Stored cloud credential name. |
+| kubeConfig | string | No | Kubeconfig contents. |
+
+**Response:** `200` — `ClusterInfo` after connecting. `400` if connecting fails (e.g.
+existing user auth on a non-reconnect), `422` for an invalid body.
+
+---
+
+### POST /clustersv2/{owner}/{cluster}/remove
+
+Remove a cluster and its components.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| response-id | string | No | Optional response identifier for progress notifications. |
+| notify | boolean | No | Whether to send progress notifications (defaults to `true`). |
+
+**Request body:** a `RemovalOptions` object.
+
+```json
+{
+ "name": "my-cluster",
+ "components": { "fluxCD": true, "allFeatures": true }
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| name | string | No | Cluster name. |
+| components | object (`ComponentOptions`) | No | Components to remove. |
+
+**Response:** `200` when the removal task is submitted. `422` for an invalid body.
+
+---
+
+### POST /clustersv2/{owner}/{cluster}/reconfigure
+
+Reconfigure a cluster's components.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Request body:** a `ReconfigureOptions` object.
+
+```json
+{
+ "basicInfo": { "name": "my-cluster" },
+ "components": {
+ "allFeatures": false,
+ "featureSets": [ { "name": "opscenter-core", "features": ["kube-ui-server"] } ]
+ }
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| basicInfo | object (`BasicInfo`) | No | Cluster basic info. |
+| components | object (`ComponentOptions`) | No | Components to reconfigure. |
+
+**Response:** `200` — the internal cluster info record (modeled as a generic Kubernetes
+object). `422` for an invalid body.
+
+---
+
+### POST /clustersv2/{owner}/{cluster}/feature/{feature}/convert
+
+Convert a feature to be UI-managed.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+| feature | string | Name of the feature to convert. |
+
+**Response:** `200` when the feature is converted. `400` if the feature is not enabled or
+already UI-managed.
+
+---
+
+## Kubernetes version upgrades
+
+### GET /clustersv2/{owner}/{cluster}/update-version/operations
+
+List cluster version update operations.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Response:** `200` — a `ConfigMapList` of upgrade operations (an arbitrary Kubernetes
+list object).
+
+> **Verified:** `GET` returned `200` against `appscode/ace` on 2026-07-14.
+
+---
+
+### GET /clustersv2/{owner}/{cluster}/update-version/list
+
+List upgradeable Kubernetes versions for a cluster.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Response:** `200` — an array of version strings.
+
+```json
+["v1.37.0", "v1.38.0"]
+```
+
+> **Verified:** `GET` returned `500` against `appscode/ace` on 2026-07-14 — `ace` is a
+> k3s hub cluster, not a CAPI-managed cluster, so upgradeable-version discovery does not
+> apply. This endpoint targets CAPI clusters.
+
+---
+
+### GET /clustersv2/{owner}/{cluster}/update-version/{version}
+
+Upgrade a CAPI cluster to a target Kubernetes version.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+| version | string | Target Kubernetes version. |
+
+**Response:** `200` when the upgrade task is submitted.
+
+> **Note:** although this is an HTTP `GET`, it triggers a state change (a CAPI cluster
+> upgrade). It was **not** exercised against the live platform.
+
+---
+
+## Gateway configurations
+
+### GET /clustersv2/{owner}/{cluster}/gateway-configs
+
+List gateway configurations.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Response:** `200` — an array of `GatewayConfigInfo`:
+
+```json
+[
+ {
+ "presetName": "ace",
+ "presetNamespace": "ace",
+ "configName": "ace",
+ "configNamespace": "ace",
+ "version": "v2026.6.19",
+ "host": "",
+ "hostType": "ip",
+ "isDefault": true,
+ "updateAvailable": false
+ }
+]
+```
+
+| Field | Type | Description |
+|---|---|---|
+| presetName / presetNamespace | string | Gateway preset name and namespace. |
+| configName / configNamespace | string | GatewayConfig object name and namespace. |
+| version | string | Chart version of the config. |
+| host | string | Gateway host (IP or hostname). |
+| hostType | string | Host type (e.g. `ip`, `domain`). |
+| isDefault | boolean | Whether this is the default gateway. |
+| updateAvailable | boolean | Whether an update to the ace chart version is available. |
+
+> **Verified:** `GET` returned `200` against `appscode/ace` on 2026-07-14.
+
+---
+
+### PUT /clustersv2/{owner}/{cluster}/gateway-configs
+
+Update all non-`ace` gateway configurations to the ace chart version.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+**Request body:** none.
+
+**Response:** `200` when the gateway configurations are updated.
+
+---
+
+### PUT /clustersv2/{owner}/{cluster}/gateway-configs/{namespace}/{name}
+
+Update a single gateway configuration to the ace chart version.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+| namespace | string | Namespace of the GatewayConfig. |
+| name | string | Name of the GatewayConfig. |
+
+**Request body:** none.
+
+**Response:** `200` when updated. `400` if the target is the `ace` gateway config (which
+cannot be updated this way).
+
+---
+
+## Virtual clusters (vcluster)
+
+### POST /clustersv2/{owner}/vcluster/create
+
+Create a virtual cluster on a host cluster.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+
+**Request body:** a `ClusterOptions` object.
+
+```json
+{
+ "clusterName": "my-vcluster",
+ "hostClusterName": "ace",
+ "hostClusterUID": "",
+ "hostClusterOwnerID": 3,
+ "hubClusterName": "ace",
+ "hubClusterUID": "",
+ "hubClusterOwnerID": 3
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| clusterName | string | No | Name for the new virtual cluster. |
+| hostClusterName | string | No | Host cluster name. |
+| hostClusterUID | string | No | Host cluster UID. |
+| hostClusterOwnerID | integer (int64) | No | Host cluster owner ID. |
+| hubClusterName | string | No | Hub cluster name. |
+| hubClusterUID | string | No | Hub cluster UID. |
+| hubClusterOwnerID | integer (int64) | No | Hub cluster owner ID. |
+
+**Response:** `200` when the creation task is submitted. `422` for an invalid body.
+
+---
+
+### POST /clustersv2/{owner}/vcluster/import
+
+Import a virtual cluster from a host cluster.
+
+**Auth:** token.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+
+**Request body:** a `ClusterOptions` object (same schema as
+[`/vcluster/create`](#post-clustersv2ownervclustercreate)).
+
+**Response:** `200` when the import task is submitted. `422` for an invalid body.
diff --git a/docs/platform/api/cluster-management-v2/subscriptions.md b/docs/platform/api/cluster-management-v2/subscriptions.md
new file mode 100644
index 0000000..969c4bd
--- /dev/null
+++ b/docs/platform/api/cluster-management-v2/subscriptions.md
@@ -0,0 +1,189 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-cluster-management-v2-subscriptions
+ name: Subscriptions & Inbox
+ parent: api-cluster-management-v2
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Subscriptions & Inbox
+
+Inbox notification subscriptions let the current user opt in to receive notifications
+about a cluster, a namespace, or a specific resource. A companion endpoint issues an
+**inbox agent token** that a cluster's inbox agent uses to authenticate.
+
+All paths on this page are relative to `/api/v1` (the subscription routes live under
+`/api/v1/clustersv2/...`; the agent-token route is `/api/v1/agent/...`). Every endpoint
+requires a token (`Authorization: token `).
+
+Common conventions:
+
+- **owner** — the organization slug or username that owns the cluster (e.g. `appscode`).
+- **cluster** — the cluster name within that owner's scope (e.g. `ace`, `arnob-dev`).
+- Subscriptions are **per current user** — the authenticated caller is the subscriber.
+- Each subscription resource supports three verbs on the same path: `POST` to subscribe,
+ `GET` to check the subscription, and `DELETE` to unsubscribe. All return `200` on
+ success (with no body).
+
+Example request:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clustersv2/appscode/ace/subscriptions/
+```
+
+---
+
+## Cluster-level subscription
+
+Subscribe to inbox notifications for the entire cluster.
+
+Path: `/clustersv2/{owner}/{cluster}/subscriptions/`
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+
+### POST /clustersv2/{owner}/{cluster}/subscriptions/
+
+Subscribe the current user to cluster-level inbox notifications.
+
+**Auth:** token. **Response:** `200` — subscription created (no body).
+
+### GET /clustersv2/{owner}/{cluster}/subscriptions/
+
+Check the current user's cluster-level inbox subscription.
+
+**Auth:** token. **Response:** `200` — subscription exists (no body).
+
+> **Verified:** `GET` returned `500` against `appscode/ace` on 2026-07-14 — checking a
+> subscription requires the inbox/notification backend, which is not provisioned on this
+> test instance. The route is reachable and authorized (no 401/403).
+
+### DELETE /clustersv2/{owner}/{cluster}/subscriptions/
+
+Remove the cluster-level inbox subscription.
+
+**Auth:** token. **Response:** `200` — subscription removed (no body).
+
+---
+
+## Namespace-level subscription
+
+Subscribe to inbox notifications for a single namespace in the cluster.
+
+Path: `/clustersv2/{owner}/{cluster}/subscriptions/namespaces/{namespace}/`
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+| namespace | string | Namespace to subscribe to / check / unsubscribe from. |
+
+### POST /clustersv2/{owner}/{cluster}/subscriptions/namespaces/{namespace}/
+
+Subscribe the current user to namespace-level inbox notifications.
+
+**Auth:** token. **Response:** `200` — subscription created (no body).
+
+### GET /clustersv2/{owner}/{cluster}/subscriptions/namespaces/{namespace}/
+
+Check the current user's namespace-level inbox subscription.
+
+**Auth:** token. **Response:** `200` — subscription exists (no body).
+
+### DELETE /clustersv2/{owner}/{cluster}/subscriptions/namespaces/{namespace}/
+
+Remove the namespace-level inbox subscription.
+
+**Auth:** token. **Response:** `200` — subscription removed (no body).
+
+---
+
+## Resource-level subscription
+
+Subscribe to inbox notifications for one specific Kubernetes resource object, identified
+by its group/version/resource and name within a namespace.
+
+Path:
+`/clustersv2/{owner}/{cluster}/subscriptions/namespaces/{namespace}/{group}/{version}/{resource}/{resourceName}`
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| owner | string | Organization slug or username. |
+| cluster | string | Cluster name. |
+| namespace | string | Namespace of the resource. |
+| group | string | API group of the resource (use `core` or an empty-equivalent for the core group, per the resource's GVR). |
+| version | string | API version of the resource (e.g. `v1`, `v1alpha2`). |
+| resource | string | Resource plural name (e.g. `mongodbs`, `deployments`). |
+| resourceName | string | Name of the specific resource object. |
+
+Example path for a KubeDB MongoDB named `mg-shard` in namespace `demo`:
+
+```
+/clustersv2/appscode/arnob-dev/subscriptions/namespaces/demo/kubedb.com/v1/mongodbs/mg-shard
+```
+
+### POST .../{group}/{version}/{resource}/{resourceName}
+
+Subscribe the current user to resource-level inbox notifications.
+
+**Auth:** token. **Response:** `200` — subscription created (no body).
+
+### GET .../{group}/{version}/{resource}/{resourceName}
+
+Check the current user's resource-level inbox subscription.
+
+**Auth:** token. **Response:** `200` — subscription exists (no body).
+
+### DELETE .../{group}/{version}/{resource}/{resourceName}
+
+Remove the resource-level inbox subscription.
+
+**Auth:** token. **Response:** `200` — subscription removed (no body).
+
+---
+
+## Inbox agent token
+
+### GET /agent/{clusterName}/{clusterID}/token
+
+Get an inbox agent JWT token for a cluster. The cluster's inbox agent uses this token
+(and CA bundle) to authenticate when delivering notifications.
+
+**Auth:** token.
+
+> **Note:** this route lives under `/api/v1/agent/...`, not `/api/v1/clustersv2/...`, but
+> it belongs to the Cluster Management v2 group.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| clusterName | string | Cluster name. |
+| clusterID | string | Cluster UID. |
+
+**Response:** `200` — an `InboxTokenRequestResponse`:
+
+```json
+{
+ "agentJwtToken": "",
+ "caBundle": ""
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| agentJwtToken | string | JWT the inbox agent uses to authenticate. Treat as a secret. |
+| caBundle | string | Base64-encoded CA bundle for verifying the endpoint. |
diff --git a/docs/platform/api/licensing-contracts/_index.md b/docs/platform/api/licensing-contracts/_index.md
new file mode 100644
index 0000000..13a3612
--- /dev/null
+++ b/docs/platform/api/licensing-contracts/_index.md
@@ -0,0 +1,50 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-licensing-contracts
+ name: Licensing & Contracts
+ parent: api
+ weight: 120
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Licensing & Contracts
+
+Contracts bind an organization (or user) to products, clusters, and usage quotas;
+licenses are issued per contract-cluster. This group covers three related concerns:
+
+- **License registration** — the always-available endpoints that member clusters
+ (via `license-proxyserver`) use to validate a license and obtain a fresh one.
+- **Contracts (admin)** — the AppsCode-hosted, site-admin console for managing every
+ contract across accounts.
+- **Contracts (user)** — the AppsCode-hosted, token-scoped API an organization uses to
+ manage its own contracts, bind clusters, and issue offline licenses.
+
+All routes are served under the `/api/v1` prefix.
+
+> **Deployment note.** The `/contracts/*` and `/user/contracts/*` families — plus
+> `POST /user/license-proxy` — are **AppsCode-hosted-only**. On a self-hosted platform
+> these routes are not registered and return `404 Not Found`. They are documented here
+> for completeness; the verification notes below record the observed behavior on the
+> self-hosted test platform.
+
+## Pages
+
+- [License Registration](../registration.md) — `POST /register`,
+ `POST /license/issue`, `POST /user/license-proxy`.
+- [Contracts — Admin](../contracts-admin.md) — `/contracts/*`
+ (AppsCode-hosted, site-admin).
+- [Contracts — User](../contracts-user.md) — `/user/contracts/*`
+ (AppsCode-hosted, token).
+
+## Common concepts
+
+- **Owner scope.** Contract endpoints accept an owner (organization) context via the
+ optional `?org=` query parameter. When omitted, the caller's own account is used.
+- **Contract IDs** (`id`) are numeric (`int64`). A **contract-cluster ID** (`ccID`) is
+ the numeric ID of a cluster's binding within a specific contract — distinct from the
+ cluster UUID.
+- **Cluster UUID** (`clusterID` / `cluster`) is the cluster's UUID string.
+- Timestamps on the `Contract` object (`start`, `end`) are Unix seconds.
diff --git a/docs/platform/api/licensing-contracts/contracts-admin.md b/docs/platform/api/licensing-contracts/contracts-admin.md
new file mode 100644
index 0000000..f640439
--- /dev/null
+++ b/docs/platform/api/licensing-contracts/contracts-admin.md
@@ -0,0 +1,322 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-licensing-contracts-contracts-admin
+ name: Contracts — Admin
+ parent: api-licensing-contracts
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Contracts — Admin
+
+The site-admin console for managing every contract across accounts. All routes on this
+page are rooted at `/api/v1/contracts` (paths below are shown relative to that root, so
+`GET /` is `GET /api/v1/contracts`).
+
+- **Auth:** token plus **site-admin** authorization (the specific permission is noted
+ per endpoint, e.g. `view_contracts:site_admin`). **AppsCode-hosted only.**
+- **Owner scope:** every endpoint accepts the optional `?org=` query parameter to
+ scope the target account; when omitted the caller's account is used.
+
+> **Deployment note.** These routes are AppsCode-hosted-only and are **not registered on
+> self-hosted platforms**. See the verification notes below.
+
+---
+
+## Shared object: `Contract`
+
+Most read/write endpoints return a `Contract`. Its fields:
+
+| Field | Type | Description |
+|---|---|---|
+| `id` | int64 | Contract ID. |
+| `uid` | int64 | Owner account ID. |
+| `orgOrUser` | string | Owner org/user slug. |
+| `name` | string | Contract name. |
+| `product` | string | Product alias the contract covers. |
+| `features` | array of string | Licensed features. |
+| `clusters` | array of `ContractClusterStatus` | Clusters bound to the contract (see below). |
+| `associatedCluster` | `ContractClusterStatus` | The cluster currently in context, when applicable. |
+| `emails` | `ContractEmail` | Notification recipients (`contactPerson`, `signingAuthority`, `licenseUsers`, `financeTeam`, `usageAlertClient`, `usageAlertAdmin`; each a string array). |
+| `quota` | `QuotaRule` | `enabled`, `metric` (e.g. `memory_gib_month`), `limit`, `alertThresholds` (int array), `disableLicenseOnLimitReached`. |
+| `start` | int64 | Contract start time (Unix seconds). |
+| `end` | int64 | Contract end time (Unix seconds). |
+| `documentPath` | string | Stored contract-document path. |
+| `allowOffline` | boolean | Whether offline (air-gapped) licenses are permitted. |
+| `autoAssignCluster` | boolean | Auto-assign newly imported clusters. |
+| `disableAnalytics` | boolean | Disable analytics reporting. |
+| `enableClientBilling` | boolean | Enable client (managed-service) billing. |
+| `revoked` | boolean | Whether the contract is revoked. |
+| `restrictions` | string | JSON-encoded restrictions data. |
+
+`ContractClusterStatus`: `{ id (int64, the contract-cluster ID), contractId, cluster
+(UUID), displayName, clusterStatus { isImported, name, tags[], associatedContractId },
+duplicateContractFeatures[] { contractId, product, features[] }, error }`.
+
+---
+
+## `GET /` — list all contracts
+
+List all contracts across accounts (scoped by `org`).
+
+- **Auth:** site-admin (`view_contracts:site_admin`).
+- **Query:** `org` (string, optional) — owner context.
+- **Response:** `200 OK` — array of [`Contract`](#shared-object-contract).
+
+```json
+[
+ {
+ "id": 42,
+ "orgOrUser": "appscode",
+ "name": "Acme KubeDB 2026",
+ "product": "kubedb",
+ "features": ["kubedb", "kubedb-provisioner"],
+ "start": 1767225600,
+ "end": 1798761600,
+ "allowOffline": true,
+ "revoked": false
+ }
+]
+```
+
+> **Verified:** `GET /contracts?org=appscode` returned `404` against the self-hosted platform on 2026-07-14 — contracts APIs are AppsCode-hosted-only and not registered here (token confirmed valid: `GET /user` returned `200`).
+
+Errors: `401` Unauthorized, `403` Forbidden (site-admin authorization required).
+
+```bash
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/contracts?org=appscode"
+```
+
+---
+
+## `POST /` — create contract(s)
+
+Create one or more contracts (one per product listed in `products`).
+
+- **Auth:** site-admin (`create_contracts:site_admin`).
+- **Request body:** `multipart/form-data` (so an optional contract `document` file may be
+ uploaded). Fields mirror [`Contract`](#shared-object-contract) except it takes a
+ `products` array (creating one contract per product) instead of a single `product`,
+ and an optional `document` file part.
+- **Response:** `201 Created`. Errors: `400` (empty products, invalid emails,
+ unsupported product, or bad quota), `401`, `403`, `422`.
+
+---
+
+## `GET /{id}` — get a contract
+
+- **Auth:** site-admin (`view_contracts:site_admin`).
+- **Path:** `id` (int64, required) — contract ID.
+- **Response:** `200 OK` — a [`Contract`](#shared-object-contract). Errors: `401`, `403`, `404` (not found).
+
+---
+
+## `PUT /{id}` — update a contract
+
+- **Auth:** site-admin (`update_contracts:site_admin`).
+- **Path:** `id` (int64, required).
+- **Request body:** `multipart/form-data` (optional replacement `document` file); fields
+ mirror [`Contract`](#shared-object-contract).
+- **Response:** `200 OK` — updated [`Contract`](#shared-object-contract). Errors: `400`
+ (invalid quota or emails), `401`, `403`, `404`, `422`.
+
+---
+
+## `DELETE /{id}` — delete a contract
+
+- **Auth:** site-admin (`delete_contracts:site_admin`).
+- **Path:** `id` (int64, required).
+- **Response:** `200 OK`. Errors: `401`, `403`, `404`.
+
+---
+
+## `POST /{id}/extend` — extend a contract
+
+Extend a contract's end time using the `end` field of the request body.
+
+- **Auth:** site-admin (`extend_contracts:site_admin`).
+- **Path:** `id` (int64, required).
+- **Request body** (`application/json`): a `Contract` object; only `end` (Unix seconds)
+ is used.
+
+```json
+{ "end": 1830297600 }
+```
+
+- **Response:** `200 OK` — extended [`Contract`](#shared-object-contract). Errors:
+ `401`, `403`, `404`, `422`.
+
+---
+
+## `POST /{id}/revoke` — revoke a contract
+
+- **Auth:** site-admin (`revoke_contracts:site_admin`).
+- **Path:** `id` (int64, required).
+- **Response:** `200 OK`. Errors: `401`, `403`, `404`.
+
+---
+
+## `GET /{id}/document` — download contract document
+
+Return a time-limited signed URL (as a JSON string, valid 30 minutes) to download the
+contract document.
+
+- **Auth:** site-admin (`view_contracts:site_admin`).
+- **Path:** `id` (int64, required).
+- **Response:** `200 OK` — a JSON string URL, e.g. `"https://storage.example.com/…?sig="`.
+ Errors: `401`, `403`, `404`.
+
+---
+
+## `GET /{id}/audit` — contract audit history
+
+- **Auth:** contract-management (`view_contracts:contract_mgmt`).
+- **Path:** `id` (int64, required).
+- **Response:** `200 OK` — array of audit events:
+
+```json
+[
+ { "id": 1, "contractID": 42, "addedBy": "user@example.com", "event": "contract.created", "createdUnix": 1767225600 }
+]
+```
+
+Errors: `401`, `403`, `404`.
+
+---
+
+## `GET /{id}/clusters` — list bound clusters
+
+- **Auth:** site-admin (`add_cluster:site_admin`).
+- **Path:** `id` (int64, required).
+- **Response:** `200 OK` — array of `ContractClusterStatus` (see
+ [shared object](#shared-object-contract)). Errors: `401`, `403`.
+
+---
+
+## `POST /{id}/clusters` — bind clusters
+
+Bind one or more clusters to the contract.
+
+- **Auth:** site-admin (`add_cluster:site_admin`).
+- **Path:** `id` (int64, required).
+- **Request body** (`application/json`): array of cluster references.
+
+```json
+[
+ { "displayName": "prod-east", "cluster": "c122d5e0-8d6b-4025-a2ff-63f5716053b1", "tags": ["prod"] }
+]
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `cluster` | string | yes | Cluster UUID (validated as a UUID). |
+| `displayName` | string | no | Display name for the binding. |
+| `tags` | array of string | no | Tags for the cluster. |
+
+- **Response:** `201 Created` — updated `ContractClusterStatus` list. Errors: `400`
+ (empty list, invalid UUID, or already associated), `401`, `403`, `422`.
+
+---
+
+## `GET /{id}/clusters/imported/non-associated` — importable clusters
+
+List the owner's imported clusters not yet bound to the contract.
+
+- **Auth:** site-admin (`add_cluster:site_admin`).
+- **Path:** `id` (int64, required).
+- **Response:** `200 OK` — array of cluster-info records (`id`, `displayName`, `name`,
+ `uid`, `provider`, `kubernetesVersion`, `nodeCount`, `status` (a Kubernetes-style
+ `rsapi.ClusterStatus` object), and related fields). Errors: `400` (contract not
+ found), `401`, `403`.
+
+---
+
+## `DELETE /{id}/clusters/{ccID}` — remove a cluster
+
+- **Auth:** site-admin (`remove_cluster:site_admin`).
+- **Path:** `id` (int64), `ccID` (int64, the contract-cluster ID). Both required.
+- **Response:** `200 OK`. Errors: `401`, `403`.
+
+---
+
+## `POST /{id}/clusters/{ccID}/issue-license` — issue full license
+
+Issue a full (offline-capable) license for the given contract cluster.
+
+- **Auth:** site-admin (`issue_license:site_admin`).
+- **Path:** `id` (int64), `ccID` (int64). Both required.
+- **Response:** `200 OK` — a license object (same shape as
+ [`POST /license/issue`](../registration.md)): `{ contract { id,
+ startTimestamp, expiryTimestamp }, license }`. Errors: `401`, `403` (quota exceeded or
+ authorization required), `404`, `405` (revoked contract or offline not allowed).
+
+---
+
+## `PATCH /{id}/clusters/{ccID}/name` — rename a contract cluster
+
+- **Auth:** site-admin (`update_cluster_name:site_admin`).
+- **Path:** `id` (int64), `ccID` (int64). Both required.
+- **Request body** (`application/json`): `{ "clusterName": "prod-east" }`.
+- **Response:** `200 OK`. Errors: `401`, `403`, `422`.
+
+---
+
+## `PATCH /{id}/clusters/{ccID}/tags` — retag a contract cluster
+
+Replace the tag set for a cluster and sync it across contracts sharing the same cluster
+UUID.
+
+- **Auth:** site-admin (`update_cluster_tags:site_admin`).
+- **Path:** `id` (int64), `ccID` (int64). Both required.
+- **Request body** (`application/json`): `{ "tags": ["prod", "east"] }`.
+- **Response:** `200 OK`. Errors: `401`, `403`, `422`.
+
+---
+
+## `GET /active/associated-clusters/{clusterID}/status` — single-cluster status
+
+Return the association/validity status of a single cluster for the target user.
+
+- **Auth:** site-admin (`view_contracts:site_admin`).
+- **Path:** `clusterID` (string, required) — cluster UUID.
+- **Query:** `orgOrUserId` (int64, optional — target account ID; defaults to the caller
+ when omitted or `0`), `contract` (int64, optional — contract ID to check against),
+ `org` (string, optional).
+- **Response:** `200 OK` — a `ContractClusterStatus`. Errors: `401`, `403`, `500`.
+
+---
+
+## `POST /active/associated-clusters/batch-status` — batch-cluster status
+
+Return association/validity status for a batch of clusters for the target user.
+
+- **Auth:** site-admin (`view_contracts:site_admin`).
+- **Query:** `orgOrUserId` (int64, optional), `contract` (int64, optional), `org` (string, optional).
+- **Request body** (`application/json`): `{ "clusters": ["", ""] }`.
+- **Response:** `200 OK` — array of `ContractClusterStatus`. Errors: `401`, `403`, `422`, `500`.
+
+---
+
+## `GET /available_products` — products available for contracts
+
+Return the map of non-community products supported for contracts, keyed by product alias.
+
+- **Auth:** site-admin (`create_contracts:site_admin`).
+- **Query:** `org` (string, optional).
+- **Response:** `200 OK` — a JSON object mapping product alias → plan info (free-form
+ object).
+
+```json
+{
+ "kubedb": { "name": "KubeDB", "plans": ["enterprise"] },
+ "stash": { "name": "Stash", "plans": ["enterprise"] }
+}
+```
+
+> **Verified:** `GET /contracts/available_products?org=appscode` returned `404` against the self-hosted platform on 2026-07-14 — AppsCode-hosted-only route, not registered here (token confirmed valid via `GET /user` → `200`).
+
+Errors: `401`, `403`.
diff --git a/docs/platform/api/licensing-contracts/contracts-user.md b/docs/platform/api/licensing-contracts/contracts-user.md
new file mode 100644
index 0000000..9742613
--- /dev/null
+++ b/docs/platform/api/licensing-contracts/contracts-user.md
@@ -0,0 +1,296 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-licensing-contracts-contracts-user
+ name: Contracts — User
+ parent: api-licensing-contracts
+ weight: 30
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Contracts — User
+
+The token-scoped API an organization uses to manage its own contracts, bind clusters,
+and issue offline licenses. All routes on this page are rooted at
+`/api/v1/user/contracts` (paths below are relative to that root, so `GET /` is
+`GET /api/v1/user/contracts`).
+
+- **Auth:** token (`Authorization: token `). Some write endpoints require an
+ extra authorization check, noted per endpoint. **AppsCode-hosted only.**
+- **Owner scope:** every endpoint accepts the optional `?org=` query parameter to
+ scope the target account; when omitted the caller's account is used.
+
+The `Contract` and `ContractClusterStatus` object shapes are the same as documented on
+the [Contracts — Admin](../contracts-admin.md) page (user responses
+sanitize admin-only email fields).
+
+> **Deployment note.** These routes are AppsCode-hosted-only and are **not registered on
+> self-hosted platforms**. See the verification notes below.
+
+---
+
+## `GET /` — list my contracts
+
+List the contracts owned by the resolved owner.
+
+- **Auth:** token.
+- **Query:** `org` (string, optional).
+- **Response:** `200 OK` — array of `Contract`.
+
+```json
+[
+ {
+ "id": 42,
+ "orgOrUser": "appscode",
+ "name": "Acme KubeDB 2026",
+ "product": "kubedb",
+ "features": ["kubedb"],
+ "start": 1767225600,
+ "end": 1798761600,
+ "allowOffline": true,
+ "autoAssignCluster": false,
+ "revoked": false
+ }
+]
+```
+
+> **Verified:** `GET /user/contracts?org=appscode` returned `404` against the self-hosted platform on 2026-07-14 — contracts APIs are AppsCode-hosted-only and not registered here (token confirmed valid: `GET /user` returned `200`).
+
+Errors: `401`, `403`.
+
+```bash
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/user/contracts?org=appscode"
+```
+
+---
+
+## `GET /active-offline-contracts` — list active offline contracts
+
+List the owner's active offline (air-gapped) contracts.
+
+- **Auth:** token. **Query:** `org` (optional).
+- **Response:** `200 OK` — array of `Contract`.
+
+> **Verified:** `GET /user/contracts/active-offline-contracts?org=appscode` returned `404` on 2026-07-14 — AppsCode-hosted-only route, not registered on the self-hosted platform.
+
+Errors: `401`, `403`.
+
+---
+
+## `POST /assign-cluster` — assign a cluster to multiple contracts
+
+Assign a single cluster to multiple contracts owned by the caller.
+
+- **Auth:** token plus contract-management authorization (`add_cluster:contract_mgmt`).
+- **Query:** `org` (optional).
+- **Request body** (`application/json`):
+
+```json
+{
+ "clusterId": "c122d5e0-8d6b-4025-a2ff-63f5716053b1",
+ "displayName": "prod-east",
+ "contractIds": [42, 43]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `clusterId` | string | yes | Cluster UUID. |
+| `displayName` | string | no | Display name for the cluster. |
+| `contractIds` | array of int64 | yes | Contracts to assign the cluster to. |
+
+- **Response:** `200 OK`. Errors: `400` (empty contract list or invalid cluster UUID),
+ `401`, `403`, `422`.
+
+---
+
+## `GET /active/associated-with/clusters/{clusterID}` — contracts on a cluster
+
+List the owner's active contracts associated with the given cluster.
+
+- **Auth:** token.
+- **Path:** `clusterID` (string, required) — cluster UUID.
+- **Query:** `onlyOffline` (boolean, optional — restrict to offline contracts), `org` (optional).
+- **Response:** `200 OK` — array of `Contract`. Errors: `401`, `403`.
+
+---
+
+## `GET /active/not-associated-with/clusters/{clusterID}` — contracts not on a cluster
+
+List the owner's active contracts that are **not** associated with the given cluster.
+
+- **Auth:** token. **Path:** `clusterID` (string, required). **Query:** `org` (optional).
+- **Response:** `200 OK` — array of `Contract`. Errors: `401`, `403`.
+
+---
+
+## `GET /active/associated-clusters` — clusters of my active offline contracts
+
+List the unique clusters associated with the owner's offline active contracts.
+
+- **Auth:** token. **Query:** `org` (optional).
+- **Response:** `200 OK` — array of cluster records:
+
+```json
+[
+ {
+ "associatedContractIDs": [42, 43],
+ "cluster": "c122d5e0-8d6b-4025-a2ff-63f5716053b1",
+ "displayName": "prod-east",
+ "tags": ["prod"]
+ }
+]
+```
+
+> **Verified:** `GET /user/contracts/active/associated-clusters?org=appscode` returned `404` on 2026-07-14 — AppsCode-hosted-only route, not registered on the self-hosted platform (token confirmed valid via `GET /user` → `200`).
+
+Errors: `401`, `403`.
+
+---
+
+## `GET /active/associated-clusters/{clusterID}/status` — single-cluster status
+
+Return the association/validity status of a single cluster for the caller.
+
+- **Auth:** token.
+- **Path:** `clusterID` (string, required) — cluster UUID.
+- **Query:** `contract` (int64, optional — contract ID to check against), `org` (optional).
+- **Response:** `200 OK` — a `ContractClusterStatus`. Errors: `401`, `403`, `500`.
+
+---
+
+## `POST /active/associated-clusters/batch-status` — batch-cluster status
+
+Return association/validity status for a batch of clusters for the caller.
+
+- **Auth:** token.
+- **Query:** `contract` (int64, optional), `org` (optional).
+- **Request body** (`application/json`): `{ "clusters": ["", ""] }`.
+- **Response:** `200 OK` — array of `ContractClusterStatus`. Errors: `401`, `403`, `422`, `500`.
+
+---
+
+## `GET /{id}` — get one of my contracts
+
+Return a single contract owned by the caller (admin-only email fields sanitized).
+
+- **Auth:** token. **Path:** `id` (int64, required). **Query:** `org` (optional).
+- **Response:** `200 OK` — a `Contract`. Errors: `401`, `403`, `404`.
+
+---
+
+## `PUT /{id}/preferences` — update contract preferences
+
+Update the caller-editable preferences (auto-assign clusters, usage-alert client
+recipients) for one of the caller's contracts.
+
+- **Auth:** token plus authorization (`updated_preferences:contracts`).
+- **Path:** `id` (int64, required). **Query:** `org` (optional).
+- **Request body** (`application/json`):
+
+```json
+{
+ "autoAssignCluster": true,
+ "usageAlertClient": ["ops@example.com"]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `autoAssignCluster` | boolean | no | Auto-assign newly imported clusters to this contract. |
+| `usageAlertClient` | array of string | no | Usage-alert recipient emails (a single string is normalized to a one-element array). |
+
+- **Response:** `200 OK` — updated `Contract`. Errors: `400` (invalid recipients),
+ `401`, `403`, `422`.
+
+---
+
+## `GET /{id}/audit` — contract audit history
+
+- **Auth:** token. **Path:** `id` (int64, required). **Query:** `org` (optional).
+- **Response:** `200 OK` — array of audit events (`id`, `contractID`, `addedBy`,
+ `event`, `createdUnix`). Errors: `401`, `403` (not found or authorization denied).
+
+---
+
+## `GET /{id}/document` — download contract document
+
+Return a time-limited signed URL (as a JSON string, valid 30 minutes) to download the
+contract document.
+
+- **Auth:** token. **Path:** `id` (int64, required). **Query:** `org` (optional).
+- **Response:** `200 OK` — a JSON string URL. Errors: `401`, `403`, `404`.
+
+---
+
+## `GET /{id}/clusters` — list bound clusters
+
+- **Auth:** token. **Path:** `id` (int64, required). **Query:** `org` (optional).
+- **Response:** `200 OK` — array of `ContractClusterStatus`. Errors: `401`, `403`.
+
+---
+
+## `POST /{id}/clusters` — bind clusters
+
+Bind one or more clusters to one of the caller's contracts.
+
+- **Auth:** token plus authorization.
+- **Path:** `id` (int64, required). **Query:** `org` (optional).
+- **Request body** (`application/json`): array of `{ cluster (UUID, required),
+ displayName, tags[] }` — same shape as the admin bind endpoint.
+- **Response:** `201 Created` — updated `ContractClusterStatus` list. Errors: `400`
+ (empty list, invalid UUID, invalid contract, or already associated), `401`, `403`, `422`.
+
+---
+
+## `GET /{id}/clusters/imported/non-associated` — importable clusters
+
+List the owner's imported clusters not yet bound to the contract.
+
+- **Auth:** token. **Path:** `id` (int64, required). **Query:** `org` (optional).
+- **Response:** `200 OK` — array of cluster-info records (same shape as the admin
+ equivalent). Errors: `400` (contract not found), `401`, `403`.
+
+---
+
+## `DELETE /{id}/clusters/{ccID}` — remove a cluster
+
+- **Auth:** token. **Path:** `id` (int64), `ccID` (int64, contract-cluster ID). Both
+ required. **Query:** `org` (optional).
+- **Response:** `200 OK`. Errors: `401`, `403`.
+
+---
+
+## `POST /{id}/clusters/{ccID}/issue-license` — issue full license
+
+Issue a full (offline-capable) license for one of the caller's contract clusters.
+
+- **Auth:** token plus authorization (`issue-license:cluster`).
+- **Path:** `id` (int64), `ccID` (int64). Both required. **Query:** `org` (optional).
+- **Response:** `200 OK` — a license object (`{ contract { id, startTimestamp,
+ expiryTimestamp }, license }`, same shape as
+ [`POST /license/issue`](../registration.md)). Errors: `401`, `403`
+ (quota exceeded or authorization denied), `404`, `405` (revoked contract or offline
+ not allowed).
+
+---
+
+## `PATCH /{id}/clusters/{ccID}/name` — rename a contract cluster
+
+- **Auth:** token. **Path:** `id` (int64), `ccID` (int64). Both required. **Query:** `org` (optional).
+- **Request body** (`application/json`): `{ "clusterName": "prod-east" }`.
+- **Response:** `200 OK`. Errors: `401`, `403`, `422`.
+
+---
+
+## `PATCH /{id}/clusters/{ccID}/tags` — retag a contract cluster
+
+Replace the tag set for a cluster and sync it across the owner's contracts sharing the
+same cluster UUID.
+
+- **Auth:** token. **Path:** `id` (int64), `ccID` (int64). Both required. **Query:** `org` (optional).
+- **Request body** (`application/json`): `{ "tags": ["prod", "east"] }`.
+- **Response:** `200 OK`. Errors: `401`, `403`, `422`.
diff --git a/docs/platform/api/licensing-contracts/registration.md b/docs/platform/api/licensing-contracts/registration.md
new file mode 100644
index 0000000..be635bb
--- /dev/null
+++ b/docs/platform/api/licensing-contracts/registration.md
@@ -0,0 +1,153 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-licensing-contracts-registration
+ name: License Registration
+ parent: api-licensing-contracts
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# License Registration
+
+Endpoints that member clusters (via `license-proxyserver`) and the console use to
+validate a license, obtain a fresh license, and generate a proxy-server installer.
+
+All paths on this page are relative to `/api/v1` (for example, `POST /register` is
+`POST /api/v1/register`).
+
+---
+
+## `POST /register`
+
+Register / validate a licensed user against a previously issued license. Used by
+on-prem / air-gapped deployments; only available when self-registration is permitted
+for the deployment type.
+
+- **Auth:** public — no token required. The request must carry a valid `license`
+ payload, which is cryptographically verified; a license-validation middleware also
+ gates the request.
+
+**Request body** (`application/json`):
+
+```json
+{
+ "clusterUID": "c122d5e0-8d6b-4025-a2ff-63f5716053b1",
+ "features": "kubedb",
+ "caCert": "",
+ "license": ""
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `clusterUID` | string | yes | UUID of the cluster being registered. |
+| `features` | string | yes | Feature/product the license covers (empty value is rejected). |
+| `caCert` | string (base64 bytes) | no | CA certificate used to verify the license chain. |
+| `license` | string (base64 bytes) | yes | The license payload to validate. |
+
+**Response:** `200 OK` — user registered (no body). Error responses:
+
+| Status | Meaning |
+|---|---|
+| `400` | Bad request (empty `features` or unregistered `clusterUID`). |
+| `403` | Registration not supported for this deployment type. |
+| `422` | Unprocessable entity (invalid request body or license). |
+| `500` | License verification or user creation failed. |
+
+```bash
+curl -X POST https:///api/v1/register \
+ -H "Content-Type: application/json" \
+ -d '{"clusterUID":"","features":"kubedb","license":""}'
+```
+
+---
+
+## `POST /license/issue`
+
+Issue a license for the caller's cluster/product based on the caller's active contract.
+
+- **Auth:** token (`Authorization: token `, or `token`/`access_token` query
+ parameter).
+
+**Request body** (`application/json`):
+
+```json
+{
+ "cluster": "c122d5e0-8d6b-4025-a2ff-63f5716053b1",
+ "features": ["kubedb", "kubedb-provisioner"]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `cluster` | string | no | Cluster UUID the license is issued for. |
+| `features` | array of string | no | Features to include on the license. |
+
+**Response:** `200 OK` — the issued license.
+
+```json
+{
+ "contract": {
+ "id": "42",
+ "startTimestamp": "2026-01-01T00:00:00Z",
+ "expiryTimestamp": "2027-01-01T00:00:00Z"
+ },
+ "license": ""
+}
+```
+
+`contract` echoes the contract the license was issued under; `license` is the license
+certificate bytes (base64-encoded in JSON). Error responses:
+
+| Status | Meaning |
+|---|---|
+| `401` | Unauthorized. |
+| `403` | Forbidden (contract quota exceeded). |
+| `405` | Contract invalid (revoked or expired). |
+| `422` | Unprocessable entity (invalid request body). |
+| `500` | Internal server error. |
+
+```bash
+curl -X POST -H "Authorization: token $ACE_TOKEN" \
+ -H "Content-Type: application/json" \
+ https:///api/v1/license/issue \
+ -d '{"cluster":"","features":["kubedb"]}'
+```
+
+---
+
+## `POST /user/license-proxy`
+
+Generate a `license-proxyserver` installer script for the resolved organization. When
+`clusterID` / `contractIDs` are supplied an **offline** installer is generated;
+otherwise an **online** installer.
+
+- **Auth:** token plus dynamic authorization on the organization. **AppsCode-hosted only.**
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `org` | string | no | Owner (organization) context. Defaults to the caller. |
+
+**Request body** (`application/json`):
+
+```json
+{
+ "clusterID": "c122d5e0-8d6b-4025-a2ff-63f5716053b1",
+ "contractIDs": [42, 43]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `clusterID` | string | no | Cluster UUID; presence selects the offline installer. |
+| `contractIDs` | array of int64 | no | Contract IDs to bake into the offline installer. |
+
+**Response:** `200 OK` — a dynamic JSON object (`map[string]any`) containing the
+generated YAML / Helm 3 installer scripts (free-form object; keys depend on the
+installer flavor). Error responses: `401` Unauthorized, `403` Forbidden, `422`
+Unprocessable entity, `500` Internal server error.
diff --git a/docs/platform/api/marketplace/_index.md b/docs/platform/api/marketplace/_index.md
new file mode 100644
index 0000000..7f4f818
--- /dev/null
+++ b/docs/platform/api/marketplace/_index.md
@@ -0,0 +1,42 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-marketplace
+ name: Marketplace
+ parent: api
+ weight: 140
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Marketplace
+
+The Marketplace APIs integrate ACE with the public cloud marketplaces (AWS, Azure,
+GCP). They cover two distinct surfaces served by two different processes:
+
+- **Marketplace webhook service** — a **separate listener** rooted at
+ `/marketplace/api/v1` (not `/api/v1`). It receives subscription lifecycle
+ notifications from the cloud marketplaces, answers the standalone-organization
+ claimable check, and serves a version endpoint. These routes only exist when the
+ b3 binary runs its `marketplace` subcommand as part of a marketplace deployment.
+- **Metered-billing proxy** (`/api/v1/proxy/metered-billing/marketplaces/*`) — a
+ site-admin proxy on the **main** API server that forwards metered-usage reports
+ and readiness probes to the AWS Marketplace Metering / GCP Service Control APIs.
+ Each provider's routes are registered **only** for the matching deployment type
+ (`AWSMarketplaceDeployment` / `GCPMarketplaceDeployment`); on any other deployment
+ they do not exist and return `404`.
+
+Because these endpoints are gated by marketplace deployment types, most of them are
+**unavailable on a standard ACE installation**. Each page notes the gating and, for
+GET endpoints, the observed behaviour on a non-marketplace deployment.
+
+> **Note on the two listeners:** the webhook service listens on its own root
+> `/marketplace/api/v1`, which is **different** from the main server's `/api/v1`
+> prefix used everywhere else in this reference. It shares the `GET /version`
+> response shape with the main server but runs as a separate process.
+
+## Pages
+
+- [Webhook Service](../webhook-service.md) — `/marketplace/api/v1/*`: claimable check, AWS/Azure/GCP subscription notifications, and version (separate listener).
+- [Metered Billing](../metered-billing.md) — `/api/v1/proxy/metered-billing/marketplaces/*`: AWS/GCP usage reporting and readiness probes (site-admin, deployment-gated).
diff --git a/docs/platform/api/marketplace/metered-billing.md b/docs/platform/api/marketplace/metered-billing.md
new file mode 100644
index 0000000..7ee2e98
--- /dev/null
+++ b/docs/platform/api/marketplace/metered-billing.md
@@ -0,0 +1,183 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-marketplace-metered-billing
+ name: Metered Billing
+ parent: api-marketplace
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Metered Billing Proxy
+
+These endpoints live on the **main** API server under
+`/api/v1/proxy/metered-billing/marketplaces/*`. Paths on this page are shown
+relative to the `/api/v1` prefix. They proxy metered-usage reports and readiness
+probes to the underlying cloud metering APIs — **AWS Marketplace Metering** and
+**GCP Service Control**.
+
+**Authentication & authorization.** All four endpoints require a **site-admin**
+token. Send it as `Authorization: token ` (or as a `token` /
+`access_token` query parameter). Non-site-admin callers receive `403`;
+unauthenticated callers receive `401`.
+
+> **Deployment gating.** Each provider's routes are registered **only** for the
+> matching b3 deployment type:
+> - `aws/*` → `AWSMarketplaceDeployment`
+> - `gcp/*` → `GCPMarketplaceDeployment`
+>
+> On any other deployment the routes **do not exist** and requests return `404`.
+> Azure has no metering proxy wired up (Azure metering is handled via the webhook's
+> `billingDetails.resourceUsageId` on the subscription side instead).
+
+> **Availability on this platform.** The reference deployment (`appscode`) is a
+> standard, non-marketplace install, so none of these routes are registered — the
+> GET readiness probes below both returned `404`. The endpoints are documented from
+> the OpenAPI schema.
+
+---
+
+## AWS metering
+
+### POST /proxy/metered-billing/marketplaces/aws/report-usage
+
+Forward a usage report to the AWS Marketplace Metering `MeterUsage` API.
+
+- **Auth:** token, **site-admin** required.
+- **Gated by:** `AWSMarketplaceDeployment`.
+
+**Request body** (`MeterUsageInput`):
+
+```json
+{
+ "DryRun": false,
+ "ProductCode": "",
+ "Timestamp": "2026-07-14T10:00:00Z",
+ "UsageDimension": "nodes",
+ "UsageQuantity": 3,
+ "UsageAllocations": [
+ {
+ "AllocatedUsageQuantity": 3,
+ "Tags": [
+ { "Key": "cluster", "Value": "ace" }
+ ]
+ }
+ ]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `ProductCode` | string | yes | AWS Marketplace product code. |
+| `Timestamp` | string (date-time) | yes | Timestamp of the metering record. |
+| `UsageDimension` | string | yes | The metered dimension being reported. |
+| `DryRun` | boolean | no | If true, validate without recording usage. |
+| `UsageQuantity` | integer (int64) | no | Quantity of the dimension consumed. |
+| `UsageAllocations` | array (`UsageAllocation`) | no | Per-bucket allocation of the usage. |
+
+**`UsageAllocation` fields:** `AllocatedUsageQuantity` (integer, required) and an
+optional `Tags` array of `{ "Key", "Value" }` pairs.
+
+**Response — `200`** (`MeterUsageOutput`):
+
+```json
+{ "MeteringRecordId": "" }
+```
+
+**Other statuses:** `400` AWS rejected the request — e.g. customer not entitled,
+duplicate request, invalid product code, or throttling (`AwsMeteringProxyErr`
+body); `401` auth required; `403` site-admin required; `422` unprocessable body;
+`500` failed to report / unmapped AWS error code (`AwsMeteringProxyErr`).
+
+`AwsMeteringProxyErr` shape:
+
+```json
+{
+ "code": "CustomerNotEntitledException",
+ "message": "the customer is not entitled to the product",
+ "origErr": {}
+}
+```
+
+### GET /proxy/metered-billing/marketplaces/aws/check/readiness
+
+Readiness probe for the AWS metering proxy.
+
+- **Auth:** token, **site-admin** required.
+- **Gated by:** `AWSMarketplaceDeployment`.
+
+**Response:** `200` proxy ready (empty body). `401` auth required; `403` site-admin
+required.
+
+**Example**
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/proxy/metered-billing/marketplaces/aws/check/readiness?org=appscode"
+```
+
+> **Verified:** returned `404` against `appscode` on 2026-07-14 — the AWS metering
+> proxy route is not registered (this is not an `AWSMarketplaceDeployment`).
+
+---
+
+## GCP metering
+
+### POST /proxy/metered-billing/marketplaces/gcp/report-usage
+
+Forward a usage report to the GCP Service Control reporting API.
+
+- **Auth:** token, **site-admin** required.
+- **Gated by:** `GCPMarketplaceDeployment`.
+
+**Request body** (`GcpUsageReport`):
+
+```json
+{
+ "name": "services/ace.endpoints..cloud.goog",
+ "startTime": "2026-07-14T09:00:00Z",
+ "endTime": "2026-07-14T10:00:00Z",
+ "labels": { "cluster": "ace" },
+ "value": { "int64Value": 3 },
+ "dimension": "nodes",
+ "serviceName": "ace.endpoints..cloud.goog"
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `name` | string | Operation / consumer resource name. |
+| `startTime` | string (date-time) | Start of the usage window. |
+| `endTime` | string (date-time) | End of the usage window. |
+| `labels` | object (string→string) | Metric labels. |
+| `value` | object (`GcpUsageValue`) | Usage value: `int64Value` and/or `doubleValue`. |
+| `dimension` | string | The metered dimension. |
+| `serviceName` | string | GCP managed service name. |
+
+**Response — `200`:** the GCP Service Control report response (an arbitrary
+Kubernetes-style / free-form object — passed through as-is).
+
+**Other statuses:** `401` auth required; `403` site-admin required; `422`
+unprocessable body; `500` failed to report to GCP Service Control.
+
+### GET /proxy/metered-billing/marketplaces/gcp/check/readiness
+
+Readiness probe for the GCP metering proxy.
+
+- **Auth:** token, **site-admin** required.
+- **Gated by:** `GCPMarketplaceDeployment`.
+
+**Response:** `200` proxy ready (empty body). `401` auth required; `403` site-admin
+required.
+
+**Example**
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/proxy/metered-billing/marketplaces/gcp/check/readiness?org=appscode"
+```
+
+> **Verified:** returned `404` against `appscode` on 2026-07-14 — the GCP metering
+> proxy route is not registered (this is not a `GCPMarketplaceDeployment`).
diff --git a/docs/platform/api/marketplace/webhook-service.md b/docs/platform/api/marketplace/webhook-service.md
new file mode 100644
index 0000000..a91b30a
--- /dev/null
+++ b/docs/platform/api/marketplace/webhook-service.md
@@ -0,0 +1,292 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-marketplace-webhook-service
+ name: Webhook Service
+ parent: api-marketplace
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Marketplace Webhook Service
+
+> **Separate listener.** Unlike every other page in this reference, these routes are
+> **not** served under `/api/v1`. They live on a dedicated marketplace webhook
+> listener rooted at **`/marketplace/api/v1`**, run by the b3 `marketplace`
+> subcommand. Paths below are relative to that root
+> (`/marketplace/api/v1/...`). This listener exists only on marketplace
+> deployments (AWS / Azure / GCP marketplace modes).
+
+The webhook service handles inbound subscription lifecycle events pushed by the cloud
+marketplaces, answers the public "is this standalone org claimable?" check used by
+the claim flow, and exposes a version endpoint identical in shape to the main
+server's `/api/v1/version`.
+
+**Authentication model.** None of these endpoints use a platform bearer token. The
+claimable check and version endpoint are **public**. The three notification
+endpoints are authenticated by the **webhook itself** — by verifying the payload and
+either the source IP range (AWS) or a per-installer `secret` query parameter
+(Azure / GCP) — not by ACE token/session security.
+
+> **Availability on this platform.** The `/marketplace/api/v1/*` root is **not
+> served** on the reference deployment (`appscode`, a standard non-marketplace
+> install). Requests to it fall through to the web frontend and return HTTP `200`
+> with `Content-Type: text/html` (the SPA), rather than the JSON documented here.
+> The endpoints below are therefore documented from the OpenAPI schema and marked
+> as not deployed; treat the JSON examples as the shapes returned by an actual
+> marketplace deployment.
+
+---
+
+## Claimable check
+
+### GET /marketplaces/standalone-organizations/{claimID}/claimable
+
+Evaluate the claim-rules flags of a standalone (marketplace-created) organization,
+identified by its claim ID, and report whether it can currently be claimed. The
+check is proxied to the central marketplace server.
+
+- **Auth:** public (`security: []`). No token required.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|---|---|---|
+| `claimID` | string | The claim ID of the standalone organization. |
+
+**Response — `200`** (`OrgClaimableCheckResp`). Returned both when the org is
+claimable and when the retry backoff has not yet expired but the org is already
+flagged claimable.
+
+```json
+{
+ "isClaimable": true,
+ "claimID": "01HZK9"
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `isClaimable` | boolean | Whether the standalone organization can currently be claimed. |
+| `claimID` | string | Echo of the claim ID that was checked. |
+
+**Other statuses:** `404` standalone org not found; `409` already claimed; `429`
+retry backoff not yet expired — try again later; `500` error while evaluating claim
+rules. Error bodies use the marketplace `ApiError` shape:
+
+```json
+{ "message": "standalone organization already claimed" }
+```
+
+**Example**
+
+```
+curl https:///marketplace/api/v1/marketplaces/standalone-organizations//claimable
+```
+
+> **Verified:** returned `200` with `Content-Type: text/html` against the reference
+> host on 2026-07-14 — i.e. the marketplace webhook listener is **not deployed**
+> here and the request was served by the web frontend SPA (catch-all), not the
+> webhook service. Documented from schema.
+
+---
+
+## Subscription notifications (webhook callbacks)
+
+These three endpoints are the callback targets registered with each cloud
+marketplace. They are **POST** only and are called by the marketplace, not by
+platform clients — they are documented here for completeness. On a successful bind
+they return the generated installer archive **download link**.
+
+### POST /marketplaces/aws/notification/resource
+
+Handle an AWS Marketplace bind-event notification.
+
+- **Auth:** enforced by the webhook. The payload is verified (`marketplace` must be
+ `Aws`; cluster ID and options validated) and, outside dev run mode, the source IP
+ must fall within the allowed AWS EC2 IP ranges. No platform bearer-token security.
+
+**Request body** (`AwsWebhookNotification`):
+
+```json
+{
+ "eventType": "subscribe-success",
+ "eventTime": "2026-07-14T10:00:00Z",
+ "bindingInfo": {
+ "marketplace": "Aws",
+ "installerId": "inst-abc123",
+ "clusterId": "cls-abc123",
+ "AWSAccountID": "111122223333",
+ "adminUsername": "admin",
+ "adminPassword": "",
+ "domainWhiteList": ["example.com"],
+ "options": {}
+ }
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `eventType` | string | no | Marketplace event type. |
+| `eventTime` | string (date-time) | no | Time the event was emitted. |
+| `bindingInfo` | object (`BindingInfo`) | yes | Subscription binding details (see below). |
+
+**`BindingInfo` fields** (shared by AWS/Azure/GCP; embeds `InitialInput`):
+
+| Field | Type | Description |
+|---|---|---|
+| `marketplace` | string (`Aws`/`Azure`/`Gcp`) | Marketplace identifier. |
+| `installerId` | string | Installer this subscription binds to. |
+| `clusterId` | string | Target cluster ID. |
+| `AWSAccountID` | string | AWS account ID (AWS only). |
+| `adminUsername` | string | Initial admin username for the deployment. |
+| `adminPassword` | string | Initial admin password (sensitive). |
+| `domainWhiteList` | string[] | Allowed email/login domains. |
+| `options` | object | Free-form installer options. |
+
+**Response — `200`:** subscription bound; returns the installer archive link.
+
+```json
+{ "link": "https:///.../installer-archive.tgz" }
+```
+
+**Other statuses:** `400` payload / binding-info / option processing failed; `403`
+request from an unauthorized IP; `500` provisioning error (`ApiError` body).
+
+### POST /marketplaces/azure/notification/resource
+
+Handle an Azure Marketplace lifecycle notification (`PUT`, `DELETE`, `BIND`).
+
+- **Auth:** enforced by the webhook via a per-installer `secret` query parameter
+ that must match the configured marketplace API secret. No platform bearer-token
+ security.
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `secret` | string | no | Marketplace API secret used to authenticate the Azure webhook call. |
+
+**Request body** (`AzureWebhookNotification`). Note: `managedResourceGroupId` and
+`resourceIdParts` are populated server-side and are **not** part of the incoming
+JSON payload.
+
+```json
+{
+ "eventType": "BIND",
+ "applicationId": "/subscriptions//resourceGroups//providers/...",
+ "eventTime": "2026-07-14T10:00:00Z",
+ "provisioningState": "Succeeded",
+ "billingDetails": { "resourceUsageId": "" },
+ "plan": {
+ "publisher": "appscode",
+ "product": "ace",
+ "name": "standard",
+ "version": "v2026.6.19"
+ },
+ "bindingInfo": {
+ "marketplace": "Azure",
+ "installerId": "inst-abc123",
+ "clusterId": "cls-abc123"
+ }
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `eventType` | string | Lifecycle event (`PUT`, `DELETE`, `BIND`). |
+| `applicationId` | string | Azure application resource ID. |
+| `managedResourceGroupId` | string | Managed resource group (server-populated). |
+| `resourceIdParts` | object | Parsed resource-ID parts (server-populated). |
+| `eventTime` | string (date-time) | Event time. |
+| `provisioningState` | string | Azure provisioning state. |
+| `billingDetails.resourceUsageId` | string | Azure metered-billing resource usage ID. |
+| `plan` | object | Publisher / product / name / version of the plan. |
+| `error` | object | Error code/message/details, when the event reports a failure. |
+| `bindingInfo` | object (`BindingInfo`) | Subscription binding details. |
+
+**Response — `200`:** notification processed. For `PUT`/`DELETE` an empty body with
+status `200`; for `BIND` the installer archive `link` is returned.
+
+**Other statuses:** `400` invalid subscription ID / unknown event type / invalid
+binding info; `403` secret mismatch (or subscription not found while revoking
+contracts on delete); `500` processing error.
+
+### POST /marketplaces/gcp/notification/resource
+
+Handle a GCP Marketplace bind-event notification.
+
+- **Auth:** enforced by the webhook via a per-installer `secret` query parameter
+ that must match the configured marketplace API secret (bypassed only for the
+ designated GCP Marketplace test installer). No platform bearer-token security.
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `secret` | string | no | Marketplace API secret used to authenticate the GCP webhook call. |
+
+**Request body** (`GcpWebhookNotification`):
+
+```json
+{
+ "eventType": "subscribe-success",
+ "eventTime": "2026-07-14T10:00:00Z",
+ "bindingInfo": {
+ "marketplace": "Gcp",
+ "installerId": "inst-abc123",
+ "clusterId": "cls-abc123"
+ }
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `eventType` | string | no | Marketplace event type. |
+| `eventTime` | string (date-time) | no | Event time. |
+| `bindingInfo` | object (`BindingInfo`) | yes | Subscription binding details. |
+
+**Response — `200`:** subscription bound; returns the installer archive link.
+
+```json
+{ "link": "https:///.../installer-archive.tgz" }
+```
+
+**Other statuses:** `400` invalid binding info / installer lookup failure; `403`
+secret mismatch or installer already bound; `500` provisioning error.
+
+---
+
+## Version
+
+### GET /version
+
+Return the version of the marketplace webhook service. Identical in shape to the
+main server's `GET /api/v1/version`.
+
+- **Auth:** public (`security: []`).
+
+**Response — `200`** (`ServerVersion`):
+
+```json
+{ "version": "v2026.6.19" }
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `version` | string | Server application version. |
+
+**Example**
+
+```
+curl https:///marketplace/api/v1/version
+```
+
+> **Verified:** returned `200` with `Content-Type: text/html` against the reference
+> host on 2026-07-14 — the `/marketplace/api/v1` listener is **not deployed** here,
+> so the request was served by the web frontend SPA, not the webhook service. For
+> comparison, the main server's `GET /api/v1/version` returned `200` with
+> `{"version":"v2026.6.19"}` (`application/json`) — that is the shape this endpoint
+> returns on an actual marketplace deployment.
diff --git a/docs/platform/api/miscellaneous/_index.md b/docs/platform/api/miscellaneous/_index.md
new file mode 100644
index 0000000..fdfd299
--- /dev/null
+++ b/docs/platform/api/miscellaneous/_index.md
@@ -0,0 +1,29 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-miscellaneous
+ name: Miscellaneous
+ parent: api
+ weight: 180
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Miscellaneous
+
+Small utility and infrastructure endpoints exposed by the `b3` backend: the server
+version, a markdown-to-HTML renderer, the Swagger UI page, a health check, and the
+OpenID Connect discovery document. These are all public (no authentication) and back
+various parts of the ACE web console and its OIDC provider.
+
+Most of these routes live under the `/api/v1` prefix like the rest of the API. Two of
+them — the health check and the OIDC discovery document — are served at the **host
+root**, not under `/api/v1`; their full paths are documented as-is.
+
+## Pages
+
+- [Miscellaneous Endpoints](../miscellaneous.md) — server version
+ (`/api/v1/version`), markdown rendering (`/api/v1/markdown`, `/api/v1/markdown/raw`),
+ the Swagger UI (`/api/v1/swagger`), the health check (`/healthz`), and OIDC discovery
+ (`/.well-known/openid-configuration`).
diff --git a/docs/platform/api/miscellaneous/miscellaneous.md b/docs/platform/api/miscellaneous/miscellaneous.md
new file mode 100644
index 0000000..7b0538b
--- /dev/null
+++ b/docs/platform/api/miscellaneous/miscellaneous.md
@@ -0,0 +1,184 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-miscellaneous-miscellaneous
+ name: Miscellaneous Endpoints
+ parent: api-miscellaneous
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Miscellaneous Endpoints
+
+Utility endpoints of the `b3` backend. Unless noted otherwise, paths on this page are
+relative to `/api/v1` — the full base path is `https:///api/v1`. Two
+endpoints (`/healthz` and `/.well-known/openid-configuration`) are served at the
+**host root** and are shown with their full path.
+
+All endpoints on this page are **public** — no authentication is required.
+
+## Server version
+
+### GET /version
+
+Returns the version of the server application.
+
+- **Auth:** Public (no authentication).
+
+**Response:** `200 OK` with a JSON object.
+
+```json
+{
+ "version": "v2026.6.19"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `version` | string | The `b3` server application version. |
+
+Example:
+
+```
+curl https:///api/v1/version
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14, body `{"version":"v2026.6.19"}`.
+
+## Markdown rendering
+
+### POST /markdown
+
+Renders the supplied markdown as HTML. In `gfm` (GitHub Flavored Markdown) mode,
+relative links are resolved against the provided `Context` URL.
+
+- **Auth:** Public (no authentication).
+
+**Request body** (`application/json`), built from the `MarkdownOption` schema:
+
+```json
+{
+ "Text": "# Hello\n\nSee [the docs](guides/intro.md).",
+ "Mode": "gfm",
+ "Context": "https://github.com/appscode/docs/blob/master/",
+ "Wiki": false
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `Text` | string | yes | Markdown to render. |
+| `Mode` | string | no | Rendering mode (e.g. `gfm`). |
+| `Context` | string | no | URL context for resolving relative links. |
+| `Wiki` | boolean | no | Whether the document is a wiki page. |
+
+**Response:** `200 OK` with `Content-Type: text/html` — the rendered HTML fragment as
+the response body. A `422` is returned on a validation error.
+
+```html
+
+```
+
+### POST /markdown/raw
+
+Renders a raw markdown request body (sent as `text/plain`) as HTML. This is the
+"raw" variant of `POST /markdown` — the whole request body is treated as the markdown
+text, with no JSON envelope or options.
+
+- **Auth:** Public (no authentication).
+
+**Request body** (`text/plain`): the raw markdown text to render.
+
+```
+# Release notes
+
+- First item
+- Second item
+```
+
+**Response:** `200 OK` with `Content-Type: text/html` — the rendered HTML fragment. A
+`422` is returned on a validation error.
+
+```html
+
Release notes
+
+
First item
+
Second item
+
+```
+
+## Swagger UI
+
+### GET /swagger
+
+Serves the Swagger UI HTML page for the v1 API. This route is only registered when
+Swagger is enabled in the server configuration; if disabled, the route is not present.
+
+- **Auth:** Public (no authentication).
+
+**Response:** `200 OK` with `Content-Type: text/html` — the Swagger UI page (an HTML
+document that loads the interactive API explorer).
+
+Example:
+
+```
+curl https:///api/v1/swagger
+```
+
+> **Verified:** `GET` returned `200` (Swagger UI HTML page) against the platform on 2026-07-14; Swagger is enabled on this deployment.
+
+## Health & OIDC discovery (host root)
+
+The following two endpoints are **not** under the `/api/v1` prefix — they are served
+at the host root. Use their full paths.
+
+### GET /healthz
+
+Liveness/health check for the server.
+
+- **Auth:** Public (no authentication).
+
+**Response:** `200 OK` when the server is healthy.
+
+Example:
+
+```
+curl https:///healthz
+```
+
+> **Verified:** `GET https:///healthz` returned `200` on 2026-07-14. On this deployment the host root serves the ACE web console single-page app, so the response body was the console HTML (catch-all) rather than a plain health payload; the `200` still confirms the server is reachable and healthy.
+
+### GET /.well-known/openid-configuration
+
+Standard OpenID Connect discovery document. `b3` is itself an OIDC provider (for SSO),
+and this endpoint advertises its issuer and the authorization/token/userinfo/JWKS
+endpoints so OIDC clients can auto-configure.
+
+- **Auth:** Public (no authentication).
+
+**Response:** `200 OK` with the OIDC discovery JSON (issuer, endpoint URLs, supported
+scopes, response types, and signing algorithms), for example:
+
+```json
+{
+ "issuer": "https://",
+ "authorization_endpoint": "https:///login/oauth/authorize",
+ "token_endpoint": "https:///login/oauth/access_token",
+ "userinfo_endpoint": "https:///login/oauth/userinfo",
+ "jwks_uri": "https:///login/oauth/keys",
+ "response_types_supported": ["code"],
+ "subject_types_supported": ["public"],
+ "id_token_signing_alg_values_supported": ["RS256"]
+}
+```
+
+Example:
+
+```
+curl https:///.well-known/openid-configuration
+```
+
+> **Verified:** `GET https:///.well-known/openid-configuration` returned `200` on 2026-07-14. On this deployment the host root serves the ACE web console single-page app, so the request was answered by the console catch-all (HTML) rather than the OIDC discovery JSON documented above. The example above reflects the endpoint's documented shape; the exact endpoint URLs vary by deployment.
diff --git a/docs/platform/api/monitoring-telemetry/_index.md b/docs/platform/api/monitoring-telemetry/_index.md
new file mode 100644
index 0000000..8ae4616
--- /dev/null
+++ b/docs/platform/api/monitoring-telemetry/_index.md
@@ -0,0 +1,41 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-monitoring-telemetry
+ name: Monitoring & Telemetry
+ parent: api
+ weight: 150
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Monitoring & Telemetry API
+
+The Monitoring & Telemetry API provisions and wires up the observability plane of the
+AppsCode Container Engine (ACE) platform. It has two surfaces:
+
+- **Telemetry stack** (`/api/v1/telemetry/...`) — designates one of an owner's imported
+ clusters as a *monitoring cluster*, installs the OpenTelemetry-based telemetry stack
+ (`appscode-otel-stack` / `opentelemetry-kube-stack`) on it, and generates the Helm
+ values that target clusters use to ship metrics, logs, and traces to that monitoring
+ cluster. It also exposes the stack's ingress host and lists the tenants served.
+- **Trickster / Prometheus auth proxy** (`/api/v1/trickster/...`) — registers and
+ unregisters per-cluster Prometheus backends behind the Trickster proxy (for Grafana or
+ Perses datasources) and provides the ownership/auth hook that `prom-authproxy` calls to
+ enforce per-tenant access to Prometheus data.
+
+> **Auth.** Every route in this group requires a valid bearer token
+> (`Authorization: token `, or the `?token=` / `?access_token=` query forms) and
+> org-admin context: the caller must be an **admin of the `{owner}` organization** (or,
+> for the Trickster routes, a member/owner of the org that owns the referenced cluster).
+> These routes are registered in all deployment modes.
+
+## Pages
+
+- [Telemetry Stack](../telemetry-stack.md) — list monitoring
+ clusters, install the telemetry stack, fetch `appscode-otel-stack` Helm values, get the
+ stack host, and list tenant owners.
+- [Trickster Auth Proxy](../trickster.md) — register/unregister
+ Prometheus backends for Grafana and Perses datasources, and the `prom-authproxy`
+ ownership check.
diff --git a/docs/platform/api/monitoring-telemetry/telemetry-stack.md b/docs/platform/api/monitoring-telemetry/telemetry-stack.md
new file mode 100644
index 0000000..c07870e
--- /dev/null
+++ b/docs/platform/api/monitoring-telemetry/telemetry-stack.md
@@ -0,0 +1,222 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-monitoring-telemetry-telemetry-stack
+ name: Telemetry Stack
+ parent: api-monitoring-telemetry
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Telemetry Stack
+
+These endpoints manage the telemetry (observability) stack of an organization. A
+**monitoring cluster** is one of the owner's imported clusters that has been designated to
+receive telemetry from other clusters. The APIs here list monitoring clusters, install the
+OpenTelemetry-based telemetry stack on a cluster, generate the `appscode-otel-stack` Helm
+values that *target* clusters use to ship data to a monitoring cluster, expose the
+monitoring cluster's ingress host, and list the tenants it serves.
+
+All paths below are relative to the API base `/api/v1`; the full prefix for this page is
+`/api/v1/telemetry`.
+
+**Auth for all endpoints on this page:** bearer **token** plus **org-admin** context — the
+caller must be an admin of the `{owner}` organization. `{owner}` is the slug of an
+organization or user that owns the cluster.
+
+A documented call looks like:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ "https:///api/v1/telemetry/appscode/monitoring-clusters?targetClusterName=arnob-dev"
+```
+
+---
+
+## Monitoring clusters
+
+### GET /telemetry/{owner}/monitoring-clusters
+
+Returns the names of clusters imported by the owner that are configured as monitoring
+clusters, optionally filtered to the region of a target cluster.
+
+- **Auth:** token + org admin (admin of `{owner}`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `owner` | string | Organization (or user) slug that owns the clusters. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `targetClusterName` | string | No* | Name of the target cluster used to determine the region filter. Only monitoring clusters in the same region as this cluster are returned. |
+
+> \* Marked optional in the schema, but the region-resolution step needs a resolvable
+> target cluster. In practice call it with `targetClusterName` set; omitting it can fail
+> to resolve cluster info (see verification note below).
+
+**Response `200`:** a JSON array of monitoring cluster names.
+
+```json
+["arnob-monitoring"]
+```
+
+> **Verified:** `GET` returned `200` against `appscode/arnob-monitoring` (with
+> `?targetClusterName=arnob-dev`) on 2026-07-14. Without `targetClusterName` the call
+> returned `500` (`cluster info not found`) because the region filter could not resolve a
+> target cluster.
+
+---
+
+### POST /telemetry/{owner}/{cluster}/stack
+
+Creates a `TelemetryStack` custom resource on the given cluster and marks that cluster as a
+monitoring cluster. This is the install step for the telemetry stack.
+
+- **Auth:** token + org admin (admin of `{owner}`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `owner` | string | Organization (or user) slug that owns the cluster. |
+| `cluster` | string | Name of the cluster to turn into a monitoring cluster. |
+
+**Request body** (`CreateTelemetryStackRequest`):
+
+```json
+{
+ "name": "appscode-otel-stack",
+ "namespace": "monitoring",
+ "spec": {
+ "...": "TelemetryStackSpec (go.open-pulse.dev/tenant-operator)"
+ }
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | string | No | Name of the `TelemetryStack` resource to create. |
+| `namespace` | string | No | Namespace in which to create it. |
+| `spec` | object | No | A `TelemetryStackSpec` (from `go.open-pulse.dev/tenant-operator`), modeled as a free-form Kubernetes object. Its fields are passed through to the cluster verbatim; see the tenant-operator TelemetryStack concept rather than treating these as fixed API fields. |
+
+**Response `200`:** the created `TelemetryStack` object — an arbitrary Kubernetes object
+returned verbatim from the cluster.
+
+```json
+{
+ "apiVersion": "monitoring.open-pulse.dev/v1alpha1",
+ "kind": "TelemetryStack",
+ "metadata": { "name": "appscode-otel-stack", "namespace": "monitoring" },
+ "spec": { "...": "..." }
+}
+```
+
+> Not verified live — this is a `POST` (mutating) request and is documented from the schema
+> only.
+
+---
+
+### GET /telemetry/{owner}/{cluster}/values/appscode-otel-stack
+
+Returns the Helm values for the `opentelemetry-kube-stack` chart to install on a *target*
+cluster so that it ships telemetry to the given monitoring cluster.
+
+- **Auth:** token + org admin (admin of `{owner}`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `owner` | string | Organization (or user) slug. |
+| `cluster` | string | Name of the monitoring cluster the values point at. |
+
+**Query parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `targetClusterName` | string | Yes | Name of the target cluster the values are generated for. |
+
+**Response `200`:** a dynamic Helm values object for the `appscode-otel-stack` chart
+(an arbitrary Kubernetes/values object). Shape depends on the chart; a truncated,
+sanitized example:
+
+```json
+{
+ "opentelemetry-kube-stack": {
+ "clusterName": "arnob-dev",
+ "collectors": {
+ "cluster": {
+ "config": {
+ "exporters": {
+ "clickhouse": {
+ "endpoint": "https://prom-label-proxy.monitoring.svc.cluster.local:10001/?database=default&secure=true&http_path=/api/v1/logs"
+ },
+ "prometheusremotewrite": {
+ "endpoint": "https://prom-label-proxy.monitoring.svc.cluster.local:10001/api/v1/receive"
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+> **Verified:** `GET` returned `200` against `appscode/arnob-monitoring` (with
+> `?targetClusterName=arnob-dev`) on 2026-07-14.
+
+---
+
+### GET /telemetry/{owner}/{cluster}/stack/host
+
+Returns the ingress host (hostname or IP) of the given monitoring cluster, used by
+telemetry collectors as their upstream endpoint.
+
+- **Auth:** token + org admin (admin of `{owner}`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `owner` | string | Organization (or user) slug. |
+| `cluster` | string | Name of the monitoring cluster. |
+
+**Response `200`:** a JSON string with the host.
+
+```json
+"10.2.0.136"
+```
+
+> **Verified:** `GET` returned `200` against `appscode/arnob-monitoring` on 2026-07-14.
+
+---
+
+### GET /telemetry/{owner}/{cluster}/tenants/ownerlist
+
+Lists the `Tenant` custom resources present on the given monitoring cluster, returning a
+map of tenant owner identifier (`ace.user.{id}`) to the ACE user/organization name.
+
+- **Auth:** token + org admin (admin of `{owner}`).
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `owner` | string | Organization (or user) slug. |
+| `cluster` | string | Name of the monitoring cluster. |
+
+**Response `200`:** a JSON object mapping tenant owner id to name.
+
+```json
+{
+ "ace.user.3": "appscode"
+}
+```
+
+> **Verified:** `GET` returned `200` against `appscode/arnob-monitoring` on 2026-07-14.
diff --git a/docs/platform/api/monitoring-telemetry/trickster.md b/docs/platform/api/monitoring-telemetry/trickster.md
new file mode 100644
index 0000000..70d6ed1
--- /dev/null
+++ b/docs/platform/api/monitoring-telemetry/trickster.md
@@ -0,0 +1,250 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-monitoring-telemetry-trickster
+ name: Trickster Auth Proxy
+ parent: api-monitoring-telemetry
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Trickster Auth Proxy
+
+These endpoints manage the **Trickster** Prometheus proxy that fronts per-cluster
+Prometheus backends and enforces per-tenant access. There are three concerns:
+
+- **Register / unregister** a Prometheus backend for a cluster so it becomes available as a
+ **Grafana** datasource (`/register`, `/unregister`).
+- The **Perses** equivalents (`/perses/register`, `/perses/unregister`) that produce a
+ Perses datasource instead of a Grafana one.
+- The **ownership/auth hook** (`/auth/{uidcid}/{path}`) that `prom-authproxy` calls to
+ verify that the caller owns (or belongs to the org that owns) the cluster identified by a
+ `uid.clusterid[.projectid]` identity before proxying a Prometheus request.
+
+All paths below are relative to the API base `/api/v1`; the full prefix for this page is
+`/api/v1/trickster`.
+
+**Auth for all endpoints on this page:** bearer **token** plus **org-admin** context. For
+the register/unregister routes the referenced cluster (via `clusterUID`) must belong to the
+caller's org; for the auth hook the caller must be a member/owner of the org that owns the
+identified cluster.
+
+A documented call looks like:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/trickster/auth/./api/v1/query
+```
+
+---
+
+## Grafana datasource registration
+
+### POST /trickster/register
+
+Registers a Prometheus backend with the Trickster proxy and returns a Grafana datasource
+response.
+
+- **Auth:** token + org admin.
+
+**Request body** (`RegisterRequest`):
+
+```json
+{
+ "prometheus": {
+ "url": "https://prometheus.monitoring.svc:9090",
+ "service": {
+ "scheme": "https",
+ "name": "prometheus",
+ "namespace": "monitoring",
+ "port": "9090",
+ "path": "/",
+ "query": ""
+ },
+ "basicAuth": { "username": "user", "password": "" },
+ "bearerToken": "",
+ "tls": {
+ "ca": "",
+ "cert": "",
+ "key": "",
+ "serverName": "prometheus.monitoring.svc",
+ "insecureSkipTLSVerify": false
+ }
+ },
+ "hubUID": "",
+ "clusterUID": "",
+ "projectId": "",
+ "default": true,
+ "issueToken": true,
+ "clientOrgID": ""
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `prometheus` | object (`PrometheusConfig`) | Yes | Connection config for the Prometheus backend (see fields below). |
+| `hubUID` | string | No | UID of the hub cluster. |
+| `clusterUID` | string | No | UID of the cluster whose Prometheus is being registered. |
+| `projectId` | string | No | Optional project scope for the identity. |
+| `default` | boolean | No | Whether this becomes the default datasource. |
+| `issueToken` | boolean | No | Whether to issue an access token for the datasource. |
+| `clientOrgID` | string | No | Client-organization id when registering on behalf of a client org. |
+
+`PrometheusConfig` fields: `url` (string), `service` (`ServiceSpec`: `scheme`, `name`,
+`namespace`, `port`, `path`, `query`), `basicAuth` (`username`, `password`), `bearerToken`
+(string), and `tls` (`ca`, `cert`, `key`, `serverName`, `insecureSkipTLSVerify`).
+
+**Response `200`** (`GrafanaDatasourceResponse`):
+
+```json
+{
+ "grafana": {
+ "url": "https:///api/v1/trickster/...",
+ "service": { "scheme": "https", "name": "trickster", "namespace": "monitoring", "port": "8480", "path": "/", "query": "" },
+ "basicAuth": { "username": "user", "password": "" },
+ "bearerToken": "",
+ "tls": { "insecureSkipTLSVerify": false },
+ "dashboard": { "datasource": ".", "folderID": 0 }
+ },
+ "folderID": 0,
+ "datasource": "."
+}
+```
+
+The `grafana` object is a `GrafanaConfig` (same connection fields as `PrometheusConfig`
+plus a `dashboard` `DashboardSpec` of `datasource`/`folderID`). The top-level `folderID`
+and `datasource` mirror the resolved Grafana context.
+
+> Not verified live — `POST` (mutating). Documented from the schema only.
+
+---
+
+### POST /trickster/unregister
+
+Removes the Trickster backend and Grafana datasource associated with the given Prometheus
+context.
+
+- **Auth:** token + org admin.
+
+**Request body** (`PrometheusContext`):
+
+```json
+{
+ "hubUID": "",
+ "clusterUID": "",
+ "projectId": "",
+ "default": true,
+ "issueToken": false,
+ "clientOrgID": ""
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `hubUID` | string | No | UID of the hub cluster. |
+| `clusterUID` | string | No | UID of the cluster whose backend is being removed. |
+| `projectId` | string | No | Optional project scope. |
+| `default` | boolean | No | Whether this was the default datasource. |
+| `issueToken` | boolean | No | Token-issuance flag from registration. |
+| `clientOrgID` | string | No | Client-organization id, if applicable. |
+
+**Response `200`:** backend unregistered (no body).
+
+> Not verified live — `POST` (mutating). Documented from the schema only.
+
+---
+
+## Perses datasource registration
+
+### POST /trickster/perses/register
+
+Registers a Prometheus backend with the Trickster proxy and returns a **Perses** datasource
+response. Same request body as `/trickster/register`.
+
+- **Auth:** token + org admin.
+
+**Request body** (`RegisterRequest`): identical to
+[`POST /trickster/register`](#post-tricksterregister).
+
+**Response `200`** (`PersesDatasourceResponse`):
+
+```json
+{
+ "perses": {
+ "url": "https:///api/v1/trickster/...",
+ "service": { "scheme": "https", "name": "trickster", "namespace": "monitoring", "port": "8480", "path": "/", "query": "" },
+ "basicAuth": { "username": "user", "password": "" },
+ "bearerToken": "",
+ "tls": { "insecureSkipTLSVerify": false },
+ "dashboard": { "projectName": "appscode", "folderName": "default", "datasource": "." }
+ },
+ "projectName": "appscode",
+ "folderName": "default",
+ "datasource": "."
+}
+```
+
+The `perses` object is a `PersesConfig` (connection fields plus a `dashboard`
+`PersesDashboardSpec` of `projectName`/`folderName`/`datasource`). The top-level
+`projectName`, `folderName`, and `datasource` mirror the resolved Perses context.
+
+> Not verified live — `POST` (mutating). Documented from the schema only.
+
+---
+
+### POST /trickster/perses/unregister
+
+Removes the Trickster backend and Perses datasource associated with the given Prometheus
+context. Same request body as `/trickster/unregister`.
+
+- **Auth:** token + org admin.
+
+**Request body** (`PrometheusContext`): identical to
+[`POST /trickster/unregister`](#post-tricksterunregister).
+
+**Response `200`:** backend unregistered (no body).
+
+> Not verified live — `POST` (mutating). Documented from the schema only.
+
+---
+
+## Auth hook
+
+### GET /trickster/auth/{uidcid}/{path}
+
+Auth-proxy hook used by `prom-authproxy`. Validates that the caller owns (or is a member of
+the org that owns) the cluster identified by the `uid.clusterid[.projectid]` segment and
+returns owner/cluster/tenant metadata. The trailing wildcard captures the proxied request
+path.
+
+- **Auth:** token + org admin — the caller must be a member of the organization that owns
+ the identified cluster.
+
+**Path parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `uidcid` | string | Identity following the `uid.clusterid[.projectid]` pattern (e.g. `3.`). |
+| `path` | string | Remaining proxied request path, matched by the trailing wildcard (e.g. `api/v1/query`). |
+
+**Response `200`:** a JSON object of string values containing the keys `owner`,
+`clusterName`, `tenantID`, and optionally `clientOrg`.
+
+```json
+{
+ "owner": "appscode",
+ "clusterName": "arnob-monitoring",
+ "tenantID": "ace.user.3",
+ "clientOrg": ""
+}
+```
+
+Other responses: `400` (malformed `uidcid`), `401` (caller is not an org member), `403`
+(forbidden), `404` (cluster or owner not found).
+
+> **Verified:** returned `404` against `appscode` on 2026-07-14 when probed with a
+> fabricated identity (`/trickster/auth/1.2/api/v1/query` → `user does not exist`), which
+> confirms the endpoint is live and enforcing identity resolution. A `200` requires a real
+> `uid.clusterid` for a cluster owned by the caller's org.
diff --git a/docs/platform/api/multicluster-ocm/_index.md b/docs/platform/api/multicluster-ocm/_index.md
new file mode 100644
index 0000000..12a79a8
--- /dev/null
+++ b/docs/platform/api/multicluster-ocm/_index.md
@@ -0,0 +1,43 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-multicluster-ocm
+ name: Multi-cluster (OCM)
+ parent: api
+ weight: 70
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Multi-cluster (OCM)
+
+APIs for Open Cluster Management (OCM) on the ACE platform: hub clusters, spoke
+(managed) clusters, cluster sets, feature sets, namespace bindings, and OCM users.
+A *hub* cluster runs the OCM control plane; *spoke* clusters register with it and
+are then grouped into *cluster sets* on which feature sets can be installed and kept
+in sync. These endpoints back the multi-cluster / hub screens of the ACE web console
+and let you script the same operations against the `b3` backend.
+
+All routes are served under the `/api/v1` prefix and are scoped to an owner and a hub
+cluster: `/api/v1/clusters/{owner}/...` (owner-only, e.g. `/hubs`) or
+`/api/v1/clusters/{owner}/{cluster}/...` where `{cluster}` is the hub cluster. The
+`owner` is an organization or user slug; `{cluster}` is the hub cluster name. Every
+endpoint authenticates with a personal access token sent as
+`Authorization: token ` (it may also be supplied as a `token` or
+`access_token` query parameter). All routes additionally resolve the owner
+organization/user and map the cluster before running; failures surface as `401`
+(unauthenticated) or `403` (not authorized for the owner/cluster).
+
+## Pages
+
+- [Hubs & Spokes](../hubs-spokes.md) — list hub clusters, inspect
+ spoke inventories (managed / accepted / not-accepted / available), accept spoke
+ join requests, generate the spoke install command, import or convert a cluster to
+ a spoke, remove a managed cluster, validate profiles, and sync account objects.
+- [Cluster Sets & Feature Sets](../cluster-sets.md) — create,
+ delete, and populate cluster sets; install/disable/update feature sets; check
+ feature sync status and auto-update; and bind namespaces to cluster sets.
+- [OCM Users](../ocm-users.md) — list, create, inspect, update, and
+ delete OCM users and their per-cluster / per-cluster-set permissions, and fetch a
+ user's kubeconfig for a spoke cluster.
diff --git a/docs/platform/api/multicluster-ocm/cluster-sets.md b/docs/platform/api/multicluster-ocm/cluster-sets.md
new file mode 100644
index 0000000..5d0c669
--- /dev/null
+++ b/docs/platform/api/multicluster-ocm/cluster-sets.md
@@ -0,0 +1,418 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-multicluster-ocm-cluster-sets
+ name: Cluster Sets & Feature Sets
+ parent: api-multicluster-ocm
+ weight: 20
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Cluster Sets & Feature Sets
+
+Cluster sets group managed (spoke) clusters so that feature sets can be installed and
+kept in sync across them, and so that namespaces can be bound to a set. All paths on
+this page are relative to the `/api/v1` prefix and are hub-scoped:
+`/api/v1/clusters/{owner}/{cluster}/...` where `owner` is an organization or user
+slug and `{cluster}` is the OCM hub cluster (for example `ace`). Every endpoint
+requires a personal access token (`Authorization: token `) and authorizes
+the caller against the owner and cluster.
+
+Common path parameters:
+
+| Name | Type | Description |
+|---|---|---|
+| `owner` | string | Organization or user slug that owns the cluster. |
+| `cluster` | string | Hub cluster name. |
+| `clusterset` | string | Cluster set name (on the routes that include it). |
+
+Example call:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/clustersets
+```
+
+## Discovering cluster sets
+
+### GET /clusters/{owner}/{cluster}/clustersets
+
+List the cluster sets on the hub, with a cluster count for each.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Response:** `200` — array of `ListClusterSetResp`.
+
+```json
+[
+ { "clusterSetName": "cp-dbaas-generic", "clusterSetTitle": "Generic DBaaS", "clusters": 1 },
+ { "clusterSetName": "cp-observability-cluster", "clusterSetTitle": "Observability Cluster", "clusters": 1 },
+ { "clusterSetName": "global", "clusterSetTitle": "", "clusters": 2 }
+]
+```
+
+Fields: `clusterSetName`, `clusterSetTitle` (human title, may be empty), `clusters`
+(cluster count).
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/available-clusters
+
+List managed clusters available for use by the owner. (Documented in full on the
+[Hubs & Spokes](../hubs-spokes.md) page; returns
+`ManagedClusterInfo[]`.)
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/available-clustersets
+
+List the cluster set names available for use by the owner.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `type` | string | No | Filter mode. When `client-org`, cluster sets are further filtered for client-org eligibility. |
+
+- **Response:** `200` — array of cluster set names (strings).
+
+```json
+["cp-dbaas-generic", "cp-observability-cluster", "global"]
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+## Managing cluster sets
+
+The create/delete/add/remove endpoints all share the `ClusterSetOptions` request
+body:
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `clusters` | string[] | Depends | Managed cluster names to operate on. |
+| `clusterSet` | string | Yes | Target cluster set name. |
+
+### POST /clusters/{owner}/{cluster}/clustersets/create
+
+Create a new cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `ClusterSetOptions`.
+
+```json
+{ "clusterSet": "cp-dbaas-generic", "clusters": [] }
+```
+
+- **Response:** `200` — cluster set created. `422` on validation error.
+
+### POST /clusters/{owner}/{cluster}/clustersets/delete
+
+Delete a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `ClusterSetOptions` (`clusterSet` identifies the set).
+- **Response:** `200` — cluster set deleted. `422` on validation error.
+
+### POST /clusters/{owner}/{cluster}/clustersets/add
+
+Add clusters to a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `ClusterSetOptions`.
+
+```json
+{ "clusterSet": "cp-dbaas-generic", "clusters": ["arnob-dev"] }
+```
+
+- **Response:** `200` — clusters added to the cluster set. `422` on validation error.
+
+### POST /clusters/{owner}/{cluster}/clustersets/remove
+
+Remove clusters from a cluster set. Same `ClusterSetOptions` body as `add`.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `ClusterSetOptions`.
+- **Response:** `200` — clusters removed from the cluster set. `422` on validation
+ error.
+
+## Cluster set contents
+
+### GET /clusters/{owner}/{cluster}/clustersets/{clusterset}/clusters
+
+List the clusters belonging to a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Response:** `200` — array of `ClustersResp`.
+
+```json
+[
+ {
+ "name": "arnob-dev",
+ "displayName": "arnob-dev",
+ "clusterNode": 1,
+ "provider": "Generic",
+ "kubernetesVersion": "v1.36.2+k3s1",
+ "imported": true
+ }
+]
+```
+
+Fields: `name`, `displayName`, `clusterNode` (node count), `provider`,
+`kubernetesVersion`, `imported`.
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/clustersets/{clusterset}/clusterlist
+
+List managed cluster names that can be *added* to the cluster set (candidates not
+yet in it).
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Response:** `200` — array of managed cluster names (strings).
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/clustersets/{clusterset}/auth-info
+
+Get authorization info for a cluster set: a map of role-ref name to the list of users
+granted that role.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Response:** `200` — a free-form object mapping role-ref name to a list of users.
+ The values are the internal user model, so the response is modeled as an arbitrary
+ object rather than a fixed schema.
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+## Feature sets
+
+### GET /clusters/{owner}/{cluster}/clustersets/{clusterset}/featuresets
+
+List the feature sets available for a cluster set, including whether each is
+installed.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Response:** `200` — array of `FeatureSetInfo`.
+
+```json
+[
+ {
+ "name": "opscenter-monitoring",
+ "title": "Monitoring",
+ "description": "Prometheus-based monitoring stack",
+ "recommended": true,
+ "installed": false,
+ "requiredFeatures": ["kube-prometheus-stack"],
+ "features": [
+ {
+ "name": "kube-prometheus-stack",
+ "title": "Kube Prometheus Stack",
+ "description": "",
+ "recommended": true,
+ "installed": false,
+ "featureBlock": "monitoring"
+ }
+ ]
+ }
+]
+```
+
+Feature-set fields: `name`, `title`, `description`, `recommended`, `installed`,
+`requiredFeatures` (string[]), and `features` (array of feature objects with `name`,
+`title`, `description`, `recommended`, `installed`, `featureBlock`).
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### POST /clusters/{owner}/{cluster}/clustersets/{clusterset}/install-featureset
+
+Install a feature set on a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Request body:** a free-form Helm values / model object (`map[string]any`)
+ describing the feature set to install. The body is passed through as an arbitrary
+ Kubernetes/Helm object, so its exact fields depend on the feature set — refer to
+ the feature set's chart values.
+- **Response:** `200` — feature set installed on the cluster set. `422` on validation
+ error.
+
+### POST /clusters/{owner}/{cluster}/clustersets/{clusterset}/disable-featureset
+
+Disable a feature set on a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Request body:** `EnableFeatureOptions`.
+
+```json
+{
+ "clusterSet": "cp-dbaas-generic",
+ "featureSet": "opscenter-monitoring",
+ "features": ["kube-prometheus-stack"]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `clusterSet` | string | Yes | Cluster set name. |
+| `featureSet` | string | Yes | Feature set to disable. |
+| `features` | string[] | No | Specific features within the set to disable. |
+
+- **Response:** `200` — feature set disabled on the cluster set. `422` on validation
+ error.
+
+### POST /clusters/{owner}/{cluster}/clustersets/{clusterset}/{managedCluster}/update-featureset
+
+Update the profile binding / feature set for a single managed cluster in the set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`, plus `managedCluster` (the
+ managed cluster name).
+- **Request body:** a free-form Helm values / model object (`map[string]any`)
+ describing the feature set update. Passed through as an arbitrary Kubernetes/Helm
+ object.
+- **Response:** `200` — managed cluster profile binding updated. `422` on validation
+ error.
+
+## Feature sync status
+
+### GET /clusters/{owner}/{cluster}/clustersets/{clusterset}/sync-status/all-features
+
+Get the sync status of all features in a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Response:** `200` — a free-form nested object mapping feature set → feature →
+ sync status (in-sync and unaligned cluster names). Modeled as an arbitrary object.
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/clustersets/{clusterset}/sync-status/opscenter-features-version
+
+Get the `opscenter-features` version sync status for a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Response:** `200` — an object with the cluster set version and a per-cluster
+ version list.
+
+```json
+{
+ "clusterSetVersion": "v2026.6.19",
+ "clusters": [
+ { "name": "arnob-dev", "version": "v2026.6.19" }
+ ]
+}
+```
+
+Fields: `clusterSetVersion`, and `clusters` (array of `{ name, version }`).
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/clustersets/{clusterset}/sync-status/{feature}
+
+Get the sync status of a single feature in a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`, plus `feature` (the feature
+ name).
+- **Response:** `200` — in-sync and out-of-sync cluster names for the feature.
+
+```json
+{
+ "insyncClusters": ["arnob-dev"],
+ "outOfSyncClusters": []
+}
+```
+
+Fields: `insyncClusters` (string[]), `outOfSyncClusters` (string[]).
+
+## Auto-update
+
+### GET /clusters/{owner}/{cluster}/clustersets/{clusterset}/auto-update/check
+
+Check whether auto-update is possible for a cluster set (all spoke clusters on the
+latest version).
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Response:** `200` — an object indicating whether auto-update is possible.
+
+```json
+{ "possible": false }
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### POST /clusters/{owner}/{cluster}/clustersets/{clusterset}/auto-update
+
+Trigger auto-update of a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`, `clusterset`.
+- **Request body:** none.
+- **Response:** `200` — cluster set auto-update triggered. `400` when auto-update is
+ not possible (spoke clusters not on the latest version); the body is an `APIError`
+ with a `message`.
+
+## Namespace bindings
+
+### GET /clusters/{owner}/{cluster}/bound-namespaces
+
+List namespaces and the cluster sets bound to each.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Response:** `200` — an object mapping namespace name to an array of bound cluster
+ set names.
+
+```json
+{
+ "default": [],
+ "open-cluster-management-addon": ["global"]
+}
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### POST /clusters/{owner}/{cluster}/bound-namespaces/bind
+
+Bind a namespace to a cluster set.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `BindNamespaceOptions`.
+
+```json
+{ "clusterSet": "global", "namespace": "open-cluster-management-addon" }
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `clusterSet` | string | Yes | Cluster set to bind. |
+| `namespace` | string | Yes | Namespace to bind it to. |
+
+- **Response:** `200` — namespace bound to the cluster set. `422` on validation
+ error.
+
+### POST /clusters/{owner}/{cluster}/bound-namespaces/unbind
+
+Unbind a namespace from a cluster set. Same `BindNamespaceOptions` body as `bind`.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `BindNamespaceOptions`.
+- **Response:** `200` — namespace unbound from the cluster set. `422` on validation
+ error.
diff --git a/docs/platform/api/multicluster-ocm/hubs-spokes.md b/docs/platform/api/multicluster-ocm/hubs-spokes.md
new file mode 100644
index 0000000..4d7a3fa
--- /dev/null
+++ b/docs/platform/api/multicluster-ocm/hubs-spokes.md
@@ -0,0 +1,313 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-multicluster-ocm-hubs-spokes
+ name: Hubs & Spokes
+ parent: api-multicluster-ocm
+ weight: 10
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# Hubs & Spokes
+
+Hub-cluster discovery and spoke (managed) cluster lifecycle for Open Cluster
+Management. All paths on this page are relative to the `/api/v1` prefix and are
+scoped to an owner and a hub cluster:
+
+- Owner-scoped: `/api/v1/clusters/{owner}/...`
+- Hub-scoped: `/api/v1/clusters/{owner}/{cluster}/...`
+
+`owner` is an organization or user slug; `{cluster}` is the OCM hub cluster name
+(for example `ace`). Every endpoint requires a personal access token
+(`Authorization: token `) and authorizes the caller against the owner
+and cluster.
+
+Common path parameters:
+
+| Name | Type | Description |
+|---|---|---|
+| `owner` | string | Organization or user slug that owns the cluster. |
+| `cluster` | string | Hub cluster name (the OCM control-plane cluster). |
+
+Example call:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/hubs
+```
+
+## Hub clusters
+
+### GET /clusters/{owner}/hubs
+
+List the OCM hub clusters (OCM / OCM-MC) owned by the owner.
+
+- **Auth:** token. Authorizes the caller against `owner`.
+- **Path parameters:** `owner` (see table above).
+- **Response:** `200` — a JSON array of hub cluster summaries.
+
+```json
+[
+ {
+ "name": "ace",
+ "displayName": "ace",
+ "hubClusterUID": "",
+ "spokeClusters": 2,
+ "addOns": 0,
+ "clusterNode": 1,
+ "provider": "Generic",
+ "hubType": "OCM",
+ "locations": "",
+ "age": "43m",
+ "createdAt": "2026-07-05T06:49:31Z"
+ }
+]
+```
+
+Fields: `name`/`displayName` (hub cluster name), `ownerName`, `hubClusterUID`,
+`spokeClusters` (count of registered spokes), `addOns`, `clusterNode` (node count),
+`provider`, `hubType` (`OCM` or `OCM-MC`), `locations`, `age`, `createdAt`.
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+## Spoke inventories
+
+These endpoints read the spoke inventory from the hub cluster. Unless noted, they
+return an array of `ManagedClusterInfo` objects:
+
+| Field | Type | Description |
+|---|---|---|
+| `name` | string | Managed (spoke) cluster name. |
+| `clusterURL` | string | API server URL of the spoke cluster. |
+| `clusterUID` | string | Spoke cluster UID. |
+| `clusterSet` | string | Cluster set the spoke belongs to (when set). |
+
+### GET /clusters/{owner}/{cluster}/managed-clusters
+
+List all OCM managed clusters. Returns the raw OCM `ManagedClusterList` object from
+the hub cluster (an arbitrary Kubernetes list object, passed through verbatim).
+
+- **Auth:** token. Authorizes the caller against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Response:** `200` — a Kubernetes `ManagedClusterList` object. See the
+ [OCM ManagedCluster concept](https://open-cluster-management.io/) for the object
+ shape; it is not reshaped by the API.
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/not-accepted-clusters
+
+List managed clusters that have registered with the hub but have not yet been
+accepted.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Response:** `200` — array of `ManagedClusterInfo` (see table above).
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/accepted-clusters
+
+List managed clusters that have been accepted by the hub.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Response:** `200` — array of `ManagedClusterInfo`.
+
+```json
+[
+ {
+ "name": "arnob-dev",
+ "clusterURL": "https://10.2.0.168:6443",
+ "clusterUID": ""
+ },
+ {
+ "name": "arnob-monitoring",
+ "clusterURL": "https://10.2.0.136:6443",
+ "clusterUID": ""
+ }
+]
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+### GET /clusters/{owner}/{cluster}/available-clusters
+
+List managed clusters available for use by the owner (the subset the owner is
+entitled to consume).
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Response:** `200` — array of `ManagedClusterInfo`.
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+## Profile validation
+
+### GET /clusters/{owner}/{cluster}/validate-profile
+
+Validate the spoke clusters' feature sets against a cluster profile.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Query parameters:**
+
+| Name | Type | Required | Description |
+|---|---|---|---|
+| `profile` | string | No | Cluster profile to validate against. Defaults to `dbaas-generic`. |
+
+- **Response:** `200` — a free-form object. Contains `valid` (the string `"true"` or
+ `"false"`) and, when invalid, a `recommended-features` list.
+
+```json
+{
+ "valid": "true"
+}
+```
+
+> **Verified:** `GET` returned `200` against `appscode/ace` (hub) on 2026-07-14.
+
+## Accepting & importing spokes
+
+### POST /clusters/{owner}/{cluster}/accept-spoke
+
+Accept one or more spoke cluster join requests on the hub.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `AcceptOptions`.
+
+```json
+{
+ "clusters": ["arnob-dev", "arnob-monitoring"],
+ "skipApproveCheck": false,
+ "requesters": ["system:open-cluster-management:arnob-dev"]
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `clusters` | string[] | Yes | Names of the managed clusters to accept. |
+| `skipApproveCheck` | boolean | No | Skip the CSR approval check when accepting. |
+| `requesters` | string[] | No | Requester identities whose CSRs should be approved. |
+
+- **Response:** `200` — spoke cluster join requests accepted. `422` on validation
+ error.
+
+### POST /clusters/{owner}/{cluster}/spoke-command
+
+Generate the install command a spoke cluster runs to register with this hub.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `ClusterOptions` — identifies the spoke and its hub. Key fields:
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `clusterName` | string | Yes | Name of the spoke cluster to create/import. |
+| `hostClusterName` | string | No | Host cluster name (for virtual clusters). |
+| `hostClusterUID` | string | No | Host cluster UID. |
+| `hostClusterOwnerID` | integer | No | Owner ID of the host cluster. |
+| `hubClusterName` | string | No | Hub cluster name. |
+| `hubClusterUID` | string | No | Hub cluster UID. |
+| `hubClusterOwnerID` | integer | No | Owner ID of the hub cluster. |
+
+```json
+{
+ "clusterName": "arnob-dev",
+ "hubClusterName": "ace",
+ "hubClusterUID": ""
+}
+```
+
+- **Response:** `200` — an object with the generated command.
+
+```json
+{
+ "spoke": "helm upgrade -i ... # spoke registration command"
+}
+```
+
+### POST /clusters/{owner}/{cluster}/import-spoke
+
+Accept and import a spoke cluster in one step (registers the cluster and installs
+the requested components).
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `ImportOptions`.
+
+```json
+{
+ "basicInfo": {
+ "name": "arnob-dev",
+ "displayName": "arnob-dev",
+ "clusterUID": "",
+ "infraNamespace": "kubeops"
+ },
+ "provider": {
+ "name": "Generic",
+ "credential": "",
+ "kubeConfig": ""
+ },
+ "components": {
+ "fluxCD": true,
+ "allFeatures": false,
+ "clusterProfile": "dbaas-generic",
+ "spokeComponent": true
+ }
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `basicInfo` | object | Yes | Identity of the cluster: `name`, `displayName`, `ownerID`, `managerID`, `userID`, `clusterUID`, `hubClusterID`, `infraNamespace`. |
+| `provider` | object | No | Provider details: `name`, `credential`, `eksAuthMode` (`IRSA`/`PodIdentity`), `clusterID`, `project`, `region`, `resourceGroup`, `kubeConfig`. |
+| `components` | object | No | What to install: `fluxCD`, `featureSets`, `allFeatures`, `clusterProfile`, `monitoringClusterName`, `spokeComponent`. |
+
+- **Response:** `200` — cluster import by OCM submitted. `422` on validation error.
+
+### POST /clusters/{owner}/{cluster}/convert/spoke
+
+Convert an existing (already-imported) cluster into an OCM spoke cluster. Uses the
+same `ImportOptions` request body as `import-spoke`.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `ImportOptions` (see `import-spoke` above).
+- **Response:** `200` — cluster converted to a spoke cluster. `422` on validation
+ error.
+
+### POST /clusters/{owner}/{cluster}/remove-managed-cluster
+
+Remove a managed cluster from the hub (uninstalls the spoke agent).
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** `RemoveClusterOptions`.
+
+```json
+{
+ "clusterName": "arnob-dev"
+}
+```
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `clusterName` | string | Yes | Name of the managed cluster to remove. |
+
+- **Response:** `200` — managed cluster removed. `422` on validation error.
+
+## Account sync
+
+### POST /clusters/{owner}/{cluster}/sync-accounts
+
+Sync account objects (users and groups) from the platform to the hub cluster so that
+OCM RBAC reflects the current accounts.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Request body:** none.
+- **Response:** `200` — account objects synced.
diff --git a/docs/platform/api/multicluster-ocm/ocm-users.md b/docs/platform/api/multicluster-ocm/ocm-users.md
new file mode 100644
index 0000000..58e4343
--- /dev/null
+++ b/docs/platform/api/multicluster-ocm/ocm-users.md
@@ -0,0 +1,220 @@
+---
+layout: docs
+menu:
+ docsplatform_{{.version}}:
+ identifier: api-multicluster-ocm-ocm-users
+ name: OCM Users
+ parent: api-multicluster-ocm
+ weight: 30
+menu_name: docsplatform_{{.version}}
+section_menu_id: api
+---
+
+# OCM Users
+
+Manage OCM users on a hub cluster and their per-cluster / per-cluster-set
+permissions, and fetch a user's kubeconfig for a spoke cluster. All paths on this
+page are relative to the `/api/v1` prefix and are hub-scoped:
+`/api/v1/clusters/{owner}/{cluster}/...` where `owner` is an organization or user
+slug and `{cluster}` is the OCM hub cluster (for example `ace`). Every endpoint
+requires a personal access token (`Authorization: token `) and authorizes
+the caller against the owner and cluster.
+
+Common path parameters:
+
+| Name | Type | Description |
+|---|---|---|
+| `owner` | string | Organization or user slug that owns the cluster. |
+| `cluster` | string | Hub cluster name. |
+| `id` | integer (int64) | OCM user ID (on the `/user/{id}...` routes). |
+
+Example call:
+
+```
+curl -H "Authorization: token $ACE_TOKEN" \
+ https:///api/v1/clusters/appscode/ace/users
+```
+
+The `Permission` object is used throughout this page (in `UserOptions` and
+`UserPermissions`):
+
+| Field | Type | Description |
+|---|---|---|
+| `name` | string | Cluster or cluster set name. |
+| `isClusterSet` | boolean | Whether `name` refers to a cluster set. |
+| `roleType` | string | Role scope: `ClusterRole` or `Role`. |
+| `roleRefName` | string | Referenced role name. |
+| `roleRefNamespace` | string[] | Namespaces the role applies to. |
+| `managedClusterRoleBindingName` | string | Backing managed-cluster role binding name. |
+| `managedClusterSetRoleBindingName` | string | Backing managed-cluster-set role binding name. |
+
+## Listing users
+
+### GET /clusters/{owner}/{cluster}/users
+
+List the OCM users on the hub cluster.
+
+- **Auth:** token. Authorizes against `owner`/`cluster`.
+- **Path parameters:** `owner`, `cluster`.
+- **Response:** `200` — array of `User` objects.
+
+```json
+[
+ {
+ "id": 2,
+ "login": "user1",
+ "username": "user1",
+ "full_name": "Example User",
+ "email": "user@example.com",
+ "avatar_url": "https://secure.gravatar.com/avatar/