diff --git a/docs/rest-apis/gateway/certificate-management.md b/docs/rest-apis/gateway/certificate-management.md
index e001645928..ebc46de136 100644
--- a/docs/rest-apis/gateway/certificate-management.md
+++ b/docs/rest-apis/gateway/certificate-management.md
@@ -95,7 +95,7 @@ Upload a new TLS certificate (PEM format) to the Gateway. The certificate is loa
diff --git a/docs/rest-apis/platform-api/README.md b/docs/rest-apis/platform-api/README.md
new file mode 100644
index 0000000000..7c63c4011b
--- /dev/null
+++ b/docs/rest-apis/platform-api/README.md
@@ -0,0 +1,213 @@
+
+
WSO2 API Platform - Platform API v0.9.0
+
+This document specifies a **RESTful API** for WSO2 **API Platform** - **Platform API**.
+
+## Authentication
+
+Most API endpoints require a Bearer JWT token in the `Authorization` header. Tokens are
+validated against a configured Identity Provider (IDP) using JWKS-based signature
+verification.
+
+**Required claim**: `organization` — UUID of the caller's organization. All operations are
+automatically scoped to this organization.
+
+**Supported signing algorithms**: RS256, RS384, RS512 (RSA) and ES256, ES384, ES512 (ECDSA).
+Works with any standards-compliant IDP (Keycloak, Azure AD, Okta, Auth0, WSO2 IS, etc.).
+
+## Scope-Based Access Control
+
+Authorization is enforced via OAuth2 scopes carried in the JWT. Both read (GET) and write
+operations require specific scopes declared in the `security` field of each operation.
+
+Base URLs:
+* https://localhost:9243/api/v0.9
+* https://api.platform.com/api/v0.9
+
+License: Apache 2.0
+
+## Table of Contents
+
+### [Authentication](authentication.md)
+
+- [Overview](authentication.md#overview)
+- [Authentication Modes](authentication.md#authentication-modes)
+- [Required Claims](authentication.md#required-claims)
+- [Authorization](authentication.md#authorization)
+- [Error Responses](authentication.md#error-responses)
+- [Troubleshooting](authentication.md#troubleshooting)
+- [Scope Reference](authentication.md#scope-reference)
+
+### [Organizations](organizations.md)
+
+- [Register a new organization](organizations.md#register-a-new-organization)
+- [List organizations](organizations.md#list-organizations)
+- [Get organization by ID](organizations.md#get-organization-by-id)
+- [Check if organization exists](organizations.md#check-if-organization-exists)
+
+### [Projects](projects.md)
+
+- [Create a new project](projects.md#create-a-new-project)
+- [Get all projects for current user's organization](projects.md#get-all-projects-for-current-users-organization)
+- [Get project by ID](projects.md#get-project-by-id)
+- [Update project](projects.md#update-project)
+- [Delete project](projects.md#delete-project)
+
+### [Applications](applications.md)
+
+- [Create a new application](applications.md#create-a-new-application)
+- [Get applications for current user's organization](applications.md#get-applications-for-current-users-organization)
+- [Get application by handle](applications.md#get-application-by-handle)
+- [Update application](applications.md#update-application)
+- [Delete application](applications.md#delete-application)
+- [List application API key mappings](applications.md#list-application-api-key-mappings)
+- [Add application API key mappings](applications.md#add-application-api-key-mappings)
+- [Remove application API key mapping](applications.md#remove-application-api-key-mapping)
+- [List application associations](applications.md#list-application-associations)
+- [Add application associations](applications.md#add-application-associations)
+- [Remove application association](applications.md#remove-application-association)
+- [List application API key mappings for an association](applications.md#list-application-api-key-mappings-for-an-association)
+
+### [REST APIs](rest-apis.md)
+
+- [Get all REST APIs for an organization](rest-apis.md#get-all-rest-apis-for-an-organization)
+- [Create a new REST API](rest-apis.md#create-a-new-rest-api)
+- [Get REST API by ID](rest-apis.md#get-rest-api-by-id)
+- [Update REST API](rest-apis.md#update-rest-api)
+- [Delete REST API](rest-apis.md#delete-rest-api)
+- [Get gateways for REST API](rest-apis.md#get-gateways-for-rest-api)
+- [Add gateways for REST API](rest-apis.md#add-gateways-for-rest-api)
+- [Create API key](rest-apis.md#create-api-key)
+- [Update API key](rest-apis.md#update-api-key)
+- [Revoke API key](rest-apis.md#revoke-api-key)
+
+### [REST API Deployments](rest-api-deployments.md)
+
+- [Create and deploy a new deployment](rest-api-deployments.md#create-and-deploy-a-new-deployment)
+- [Get deployments for a REST API](rest-api-deployments.md#get-deployments-for-a-rest-api)
+- [Get deployment by ID](rest-api-deployments.md#get-deployment-by-id)
+- [Delete deployment](rest-api-deployments.md#delete-deployment)
+- [Undeploy deployment from gateway](rest-api-deployments.md#undeploy-deployment-from-gateway)
+- [Restore a previous deployment](rest-api-deployments.md#restore-a-previous-deployment)
+
+### [Gateways](gateways.md)
+
+- [Register a new gateway](gateways.md#register-a-new-gateway)
+- [List all gateways](gateways.md#list-all-gateways)
+- [Get gateway by ID](gateways.md#get-gateway-by-id)
+- [Update gateway](gateways.md#update-gateway)
+- [Delete gateway](gateways.md#delete-gateway)
+- [Get gateway policy manifest](gateways.md#get-gateway-policy-manifest)
+
+### [Gateway Tokens](gateway-tokens.md)
+
+- [List active gateway tokens](gateway-tokens.md#list-active-gateway-tokens)
+- [Rotate gateway token](gateway-tokens.md#rotate-gateway-token)
+- [Revoke gateway token](gateway-tokens.md#revoke-gateway-token)
+
+### [Gateway Policies](gateway-policies.md)
+
+- [Get synced custom policies for the current organization](gateway-policies.md#get-synced-custom-policies-for-the-current-organization)
+- [Sync a custom policy from the gateway manifest](gateway-policies.md#sync-a-custom-policy-from-the-gateway-manifest)
+- [Get a specific custom policy version](gateway-policies.md#get-a-specific-custom-policy-version)
+- [Delete a specific custom policy version](gateway-policies.md#delete-a-specific-custom-policy-version)
+
+### [LLM Providers](llm-providers.md)
+
+- [Create a new LLM provider](llm-providers.md#create-a-new-llm-provider)
+- [List all LLM providers](llm-providers.md#list-all-llm-providers)
+- [Get LLM provider by identifier](llm-providers.md#get-llm-provider-by-identifier)
+- [Update an existing LLM provider](llm-providers.md#update-an-existing-llm-provider)
+- [Delete an LLM provider](llm-providers.md#delete-an-llm-provider)
+- [List LLM proxies by provider](llm-providers.md#list-llm-proxies-by-provider)
+- [Create a new API key for an LLM provider](llm-providers.md#create-a-new-api-key-for-an-llm-provider)
+- [List API keys for an LLM provider](llm-providers.md#list-api-keys-for-an-llm-provider)
+- [Delete an API key for an LLM provider](llm-providers.md#delete-an-api-key-for-an-llm-provider)
+
+### [LLM Proxies](llm-proxies.md)
+
+- [Create a new LLM proxy](llm-proxies.md#create-a-new-llm-proxy)
+- [List all LLM proxies](llm-proxies.md#list-all-llm-proxies)
+- [Get LLM proxy by unique identifier](llm-proxies.md#get-llm-proxy-by-unique-identifier)
+- [Update an existing LLM proxy](llm-proxies.md#update-an-existing-llm-proxy)
+- [Delete an LLM proxy](llm-proxies.md#delete-an-llm-proxy)
+- [Create a new API key for an LLM proxy](llm-proxies.md#create-a-new-api-key-for-an-llm-proxy)
+- [List API keys for an LLM proxy](llm-proxies.md#list-api-keys-for-an-llm-proxy)
+- [Delete an API key for an LLM proxy](llm-proxies.md#delete-an-api-key-for-an-llm-proxy)
+
+### [LLM Provider Templates](llm-provider-templates.md)
+
+- [Create a new LLM provider template family](llm-provider-templates.md#create-a-new-llm-provider-template-family)
+- [List templates, list a family's versions, or get a single version](llm-provider-templates.md#list-templates-list-a-familys-versions-or-get-a-single-version)
+- [Create a new version by copying an existing one](llm-provider-templates.md#create-a-new-version-by-copying-an-existing-one)
+- [Get LLM provider template by id](llm-provider-templates.md#get-llm-provider-template-by-id)
+- [Update an existing LLM provider template](llm-provider-templates.md#update-an-existing-llm-provider-template)
+- [Enable or disable a template version](llm-provider-templates.md#enable-or-disable-a-template-version)
+- [Delete a template version](llm-provider-templates.md#delete-a-template-version)
+
+### [LLM Provider Deployments](llm-provider-deployments.md)
+
+- [Create and deploy a new LLM provider deployment](llm-provider-deployments.md#create-and-deploy-a-new-llm-provider-deployment)
+- [Get deployments for an LLM provider](llm-provider-deployments.md#get-deployments-for-an-llm-provider)
+- [Get LLM provider deployment by ID](llm-provider-deployments.md#get-llm-provider-deployment-by-id)
+- [Delete LLM provider deployment](llm-provider-deployments.md#delete-llm-provider-deployment)
+- [Undeploy LLM provider deployment from gateway](llm-provider-deployments.md#undeploy-llm-provider-deployment-from-gateway)
+- [Restore a previous LLM provider deployment](llm-provider-deployments.md#restore-a-previous-llm-provider-deployment)
+
+### [LLM Proxy Deployments](llm-proxy-deployments.md)
+
+- [Create and deploy a new LLM proxy deployment](llm-proxy-deployments.md#create-and-deploy-a-new-llm-proxy-deployment)
+- [Get deployments for an LLM proxy](llm-proxy-deployments.md#get-deployments-for-an-llm-proxy)
+- [Get LLM proxy deployment by ID](llm-proxy-deployments.md#get-llm-proxy-deployment-by-id)
+- [Delete LLM proxy deployment](llm-proxy-deployments.md#delete-llm-proxy-deployment)
+- [Undeploy LLM proxy deployment from gateway](llm-proxy-deployments.md#undeploy-llm-proxy-deployment-from-gateway)
+- [Restore a previous LLM proxy deployment](llm-proxy-deployments.md#restore-a-previous-llm-proxy-deployment)
+
+### [API Keys](api-keys.md)
+
+- [List API keys for the current user](api-keys.md#list-api-keys-for-the-current-user)
+
+### [MCP Proxies](mcp-proxies.md)
+
+- [Create a new MCP proxy](mcp-proxies.md#create-a-new-mcp-proxy)
+- [List all MCP proxies](mcp-proxies.md#list-all-mcp-proxies)
+- [Get MCP proxy by unique identifier](mcp-proxies.md#get-mcp-proxy-by-unique-identifier)
+- [Update an existing MCP proxy](mcp-proxies.md#update-an-existing-mcp-proxy)
+- [Delete an MCP proxy](mcp-proxies.md#delete-an-mcp-proxy)
+- [Fetch server info from MCP proxy backend services](mcp-proxies.md#fetch-server-info-from-mcp-proxy-backend-services)
+
+### [MCP Proxy Deployments](mcp-proxy-deployments.md)
+
+- [Create and deploy a new deployment for MCP proxy](mcp-proxy-deployments.md#create-and-deploy-a-new-deployment-for-mcp-proxy)
+- [Get deployments for an MCP proxy](mcp-proxy-deployments.md#get-deployments-for-an-mcp-proxy)
+- [Get deployment by ID](mcp-proxy-deployments.md#get-deployment-by-id)
+- [Delete deployment](mcp-proxy-deployments.md#delete-deployment)
+- [Undeploy deployment from gateway](mcp-proxy-deployments.md#undeploy-deployment-from-gateway)
+- [Restore a previous deployment](mcp-proxy-deployments.md#restore-a-previous-deployment)
+
+### [SubscriptionPlans](subscriptionplans.md)
+
+- [Create subscription plan](subscriptionplans.md#create-subscription-plan)
+- [List subscription plans](subscriptionplans.md#list-subscription-plans)
+- [Get subscription plan by ID](subscriptionplans.md#get-subscription-plan-by-id)
+- [Update subscription plan](subscriptionplans.md#update-subscription-plan)
+- [Delete subscription plan](subscriptionplans.md#delete-subscription-plan)
+
+### [Subscriptions](subscriptions.md)
+
+- [Create subscription](subscriptions.md#create-subscription)
+- [List subscriptions](subscriptions.md#list-subscriptions)
+- [Get subscription by ID](subscriptions.md#get-subscription-by-id)
+- [Update subscription](subscriptions.md#update-subscription)
+- [Delete subscription](subscriptions.md#delete-subscription)
+
+### [Secrets](secrets.md)
+
+- [Create a secret](secrets.md#create-a-secret)
+- [List secrets](secrets.md#list-secrets)
+- [Get a secret by handle](secrets.md#get-a-secret-by-handle)
+- [Rotate a secret value](secrets.md#rotate-a-secret-value)
+- [Delete a secret](secrets.md#delete-a-secret)
+
+### [Schemas](schemas.md)
+
diff --git a/docs/rest-apis/platform-api/api-keys.md b/docs/rest-apis/platform-api/api-keys.md
new file mode 100644
index 0000000000..f2aca1aacf
--- /dev/null
+++ b/docs/rest-apis/platform-api/api-keys.md
@@ -0,0 +1,113 @@
+
API Keys
+
+API key management operations for REST APIs and LLM Providers
+
+## List API keys for the current user
+
+
+
+`GET /me/api-keys`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/me/api-keys \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns API keys created by the caller within the organization.
+Optionally filter by one or more artifact types using a comma-separated `type` query parameter.
+The plain key value is never returned.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|type|query|array[string]|false|Comma-separated list of artifact types to filter by.|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**type**: Comma-separated list of artifact types to filter by.
+If omitted, all types are returned.
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|type|RestApi|
+|type|LlmProvider|
+|type|LlmProxy|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "list": [
+ {
+ "id": "string",
+ "displayName": "string",
+ "maskedApiKey": "string",
+ "status": "active",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "createdBy": "john.doe",
+ "updatedAt": "2019-08-24T14:15:22Z",
+ "expiresAt": "2019-08-24T14:15:22Z",
+ "issuer": "api-platform-devportal",
+ "allowedTargets": "string",
+ "artifactId": "wso2-openai-provider",
+ "artifactType": "RestApi"
+ }
+ ],
+ "count": 0,
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of API keys retrieved successfully|[UserAPIKeyListResponse](schemas.md#schemauserapikeylistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/applications.md b/docs/rest-apis/platform-api/applications.md
new file mode 100644
index 0000000000..2b3a8b6987
--- /dev/null
+++ b/docs/rest-apis/platform-api/applications.md
@@ -0,0 +1,1548 @@
+
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|body|body|[CreateApplicationRequest](schemas.md#schemacreateapplicationrequest)|true|none|
+
+> Example responses
+>
+> 201 Response
+
+```json
+{
+ "id": "my-app-handle",
+ "displayName": "GenAI Demo App",
+ "projectId": "default-project",
+ "type": "genai",
+ "description": "Sample GenAI application",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Application created successfully|[Application](schemas.md#schemaapplication)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Get applications for current user's organization
+
+
+
+`GET /applications`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/applications?projectId=default-project \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves applications belonging to the organization specified in the JWT token.
+Filters by project using the required `projectId` query parameter.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|projectId|query|string|true|**Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+|sortBy|query|string|false|Field to sort the collection by. An unrecognized value falls back to the default sort (createdAt).|
+|sortOrder|query|string|false|Sort direction applied to `sortBy`.|
+|query|query|string|false|Case-insensitive substring filter matched against the resource id (handle).|
+
+#### Detailed descriptions
+
+**projectId**: **Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|sortBy|name|
+|sortBy|createdAt|
+|sortOrder|asc|
+|sortOrder|desc|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "my-app-handle",
+ "displayName": "GenAI Demo App",
+ "projectId": "default-project",
+ "type": "genai",
+ "description": "Sample GenAI application",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Applications retrieved successfully|[ApplicationListResponse](schemas.md#schemaapplicationlistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get application by handle
+
+
+
+`GET /applications/{applicationId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/applications/{applicationId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves a specific application by handle. Access is validated against
+the organization in the JWT token.
+
+### Authentication
+
+
+
+
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|applicationId|path|string|true|**Application ID** consisting of the **handle** of the application.|
+|body|body|[Application](schemas.md#schemaapplication)|true|none|
+
+#### Detailed descriptions
+
+**applicationId**: **Application ID** consisting of the **handle** of the application.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "id": "my-app-handle",
+ "displayName": "GenAI Demo App",
+ "projectId": "default-project",
+ "type": "genai",
+ "description": "Sample GenAI application",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application updated successfully|[Application](schemas.md#schemaapplication)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete application
+
+
+
+`DELETE /applications/{applicationId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/applications/{applicationId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes an existing application by handle.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|applicationId|path|string|true|**Application ID** consisting of the **handle** of the application.|
+
+#### Detailed descriptions
+
+**applicationId**: **Application ID** consisting of the **handle** of the application.
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Application deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## List application API key mappings
+
+
+
+`GET /applications/{applicationId}/api-keys`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/applications/{applicationId}/api-keys \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Lists all API keys mapped to the specified application.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|applicationId|path|string|true|**Application ID** consisting of the **handle** of the application.|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**applicationId**: **Application ID** consisting of the **handle** of the application.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "keyId": "client-key-1",
+ "associatedEntity": {
+ "id": "pizza-api",
+ "kind": "RestApi"
+ },
+ "status": "ACTIVE",
+ "userId": "john.doe",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z",
+ "expiresAt": "2026-11-15T10:30:00Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Mapped API keys retrieved successfully|[MappedAPIKeyListResponse](schemas.md#schemamappedapikeylistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Add application API key mappings
+
+
+
+`POST /applications/{applicationId}/api-keys`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/applications/{applicationId}/api-keys \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Adds API key mappings to the specified application.
+
+> Payload
+
+```json
+{
+ "apiKeys": [
+ {
+ "keyId": "client-key-1",
+ "associatedEntity": {
+ "id": "pizza-api"
+ }
+ }
+ ]
+}
+```
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|API key mappings added successfully|[MappedAPIKeyListResponse](schemas.md#schemamappedapikeylistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Remove application API key mapping
+
+
+
+`DELETE /applications/{applicationId}/api-keys/{apiKeyId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/applications/{applicationId}/api-keys/{apiKeyId}?entityID=my-rest-api-handle \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Removes a mapped API key from the specified application.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|applicationId|path|string|true|**Application ID** consisting of the **handle** of the application.|
+|apiKeyId|path|string|true|**API Key ID** consisting of the **name** of the API key.|
+|entityID|query|string|true|**Entity ID** of the artifact associated with the API key mapping.|
+
+#### Detailed descriptions
+
+**applicationId**: **Application ID** consisting of the **handle** of the application.
+
+**apiKeyId**: **API Key ID** consisting of the **name** of the API key.
+
+**entityID**: **Entity ID** of the artifact associated with the API key mapping.
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|API key mapping removed successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## List application associations
+
+
+
+`GET /applications/{applicationId}/associations`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/applications/{applicationId}/associations \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Lists association targets mapped to the specified application.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|applicationId|path|string|true|**Application ID** consisting of the **handle** of the application.|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**applicationId**: **Application ID** consisting of the **handle** of the application.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "provider-handle",
+ "displayName": "OpenAI Provider",
+ "version": "v1.0",
+ "kind": "LlmProvider",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application associations retrieved successfully|[ApplicationAssociationListResponse](schemas.md#schemaapplicationassociationlistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Add application associations
+
+
+
+`POST /applications/{applicationId}/associations`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/applications/{applicationId}/associations \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Adds association targets to the specified application.
+
+> Payload
+
+```json
+{
+ "associations": [
+ {
+ "id": "provider-handle",
+ "kind": "LlmProvider"
+ }
+ ]
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|applicationId|path|string|true|**Application ID** consisting of the **handle** of the application.|
+|body|body|[AddApplicationAssociationsRequest](schemas.md#schemaaddapplicationassociationsrequest)|true|none|
+
+#### Detailed descriptions
+
+**applicationId**: **Application ID** consisting of the **handle** of the application.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "provider-handle",
+ "displayName": "OpenAI Provider",
+ "version": "v1.0",
+ "kind": "LlmProvider",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application associations added successfully|[ApplicationAssociationListResponse](schemas.md#schemaapplicationassociationlistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Remove application association
+
+
+
+`DELETE /applications/{applicationId}/associations/{associationId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/applications/{applicationId}/associations/{associationId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Removes an association target from the specified application.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|applicationId|path|string|true|**Application ID** consisting of the **handle** of the application.|
+|associationId|path|string|true|**Association ID** consisting of the **handle** or **UUID** of the association target.|
+
+#### Detailed descriptions
+
+**applicationId**: **Application ID** consisting of the **handle** of the application.
+
+**associationId**: **Association ID** consisting of the **handle** or **UUID** of the association target.
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Application association removed successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## List application API key mappings for an association
+
+
+
+`GET /applications/{applicationId}/associations/{associationId}/api-keys`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/applications/{applicationId}/associations/{associationId}/api-keys \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Lists API keys mapped to the specified application for the given associated target.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|applicationId|path|string|true|**Application ID** consisting of the **handle** of the application.|
+|associationId|path|string|true|**Association ID** consisting of the **handle** or **UUID** of the association target.|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**applicationId**: **Application ID** consisting of the **handle** of the application.
+
+**associationId**: **Association ID** consisting of the **handle** or **UUID** of the association target.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "keyId": "client-key-1",
+ "associatedEntity": {
+ "id": "pizza-api",
+ "kind": "RestApi"
+ },
+ "status": "ACTIVE",
+ "userId": "john.doe",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z",
+ "expiresAt": "2026-11-15T10:30:00Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Mapped API keys retrieved successfully|[MappedAPIKeyListResponse](schemas.md#schemamappedapikeylistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/authentication.md b/docs/rest-apis/platform-api/authentication.md
new file mode 100644
index 0000000000..e44ccea978
--- /dev/null
+++ b/docs/rest-apis/platform-api/authentication.md
@@ -0,0 +1,364 @@
+# Authentication
+
+## Overview
+
+Every Platform API request is authenticated with a **Bearer JWT** in the `Authorization` header and
+authorized against the **OAuth2 scopes** carried in that token. There is no Basic Auth path and no
+API-key path for this API — the access token is the only credential.
+
+```
+Authorization: Bearer
+```
+
+Two things must be true for a request to succeed:
+
+1. **Authentication** — the token's signature, expiry and issuer validate, and it carries an
+ `organization` claim identifying the caller's organization.
+2. **Authorization** — the token's `scope` claim (or, in role mode, its roles expanded to scopes)
+ satisfies the scope declared on that specific operation. Every operation page in this
+ documentation lists its required scopes under an **Authentication** heading.
+
+All operations are automatically scoped to the organization in the token. An organization ID is
+never read from a path, query or body parameter, so a token for one organization can never reach
+another organization's resources.
+
+## Authentication Modes
+
+The mode is selected once at startup with `[platform_api.auth] mode`. Only the selected mode's
+configuration section is read; the others are ignored.
+
+| Mode | How tokens are obtained | Signature verified with | Typical use |
+|---|---|---|---|
+| `file` | The built-in login endpoint issues RS256 JWTs for locally configured users | `auth.jwt.public_key_file` | Quickstart, air-gapped setup |
+| `external_token` | Tokens are minted outside the Platform API by a trusted issuer | `auth.jwt.public_key_file` | Existing token service |
+| `idp` | An external Identity Provider's token endpoint | The IDP's JWKS (`auth.idp.jwks_url`) | Production (Asgardeo, Keycloak, Auth0, Entra ID, WSO2 IS) |
+
+**Supported signing algorithms**: `RS256`, `RS384`, `RS512` (RSA) and, in IDP mode, `ES256`,
+`ES384`, `ES512` (ECDSA). Symmetric (`HS*`) and unsigned (`none`) tokens are always rejected.
+
+### Mode: `idp` (recommended for production)
+
+Tokens are validated against the IDP's JWKS endpoint. `jwks_url` and `issuer` are required.
+
+```toml
+[platform_api.auth]
+mode = "idp"
+scope_validation = true
+
+[platform_api.auth.idp]
+name = "asgardeo"
+jwks_url = "https://accounts.example.com/oauth2/jwks"
+issuer = ["https://accounts.example.com"] # list of accepted issuers
+audience = [] # accepted "aud" values; empty skips the audience check
+validation_mode = "scope" # "scope" (default) or "role"
+role_mappings = "" # path to a role→scope YAML file, used when validation_mode = "role"
+```
+
+Obtain a token from the IDP as usual (client credentials, authorization code, …) and send it as a
+Bearer token. The Platform API never issues tokens in this mode.
+
+### Mode: `file` (local users)
+
+The login endpoint authenticates a username/password against `[[platform_api.auth.file.users]]` and
+issues an RS256 JWT signed with `auth.jwt.private_key_file`. Suitable for initial or air-gapped
+setup — prefer an IDP in production.
+
+```toml
+[platform_api.auth]
+mode = "file"
+
+[platform_api.auth.file.organization]
+id = "default"
+display_name = "Default"
+uuid = "99089a17-72e0-4dd8-a2f4-c8dfbb085295" # emitted as the `organization` claim
+
+[[platform_api.auth.file.users]]
+username = "admin"
+password_hash = "$2a$12$..." # bcrypt; htpasswd -bnBC 12 "" | tr -d ':\n'
+scopes = "ap:organization:manage ap:rest_api:manage ap:gateway:manage"
+
+[platform_api.auth.jwt]
+issuer = "platform-api"
+public_key_file = "/etc/platform-api/keys/jwt_public.pem"
+private_key_file = "/etc/platform-api/keys/jwt_private.pem"
+token_ttl = "1h"
+```
+
+The user's `scopes` string becomes the `scope` claim of the issued token — trim it to restrict what
+that user can do.
+
+#### Obtaining a token
+
+`POST /api/portal/v0.9/auth/login` (form-encoded; this path bypasses the auth middleware):
+
+```shell
+curl -X POST https://localhost:9243/api/portal/v0.9/auth/login \
+ -H 'Content-Type: application/x-www-form-urlencoded' \
+ -d 'username=admin' \
+ -d 'password='
+```
+
+> 200 Response
+
+```json
+{
+ "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "expires_at": 1767225600
+}
+```
+
+Use `token` as the Bearer credential on every subsequent call. Its lifetime is `auth.jwt.token_ttl`.
+
+### Mode: `external_token`
+
+Tokens are minted by a trusted external issuer and only *verified* here, using
+`auth.jwt.public_key_file`. The Platform API does not sign anything in this mode, and
+`private_key_file` is not required. Token expiry is whatever `exp` the issuer set.
+
+## Required Claims
+
+| Claim | Required | Purpose |
+|---|---|---|
+| `organization` | Yes | UUID of the caller's organization. Every operation is scoped to it. A token without it is rejected. |
+| `scope` | Yes (in `scope` validation mode) | Space-separated list of granted scopes, checked against the operation's declared scopes. |
+| `iss` | Yes | Must match the configured issuer (`auth.jwt.issuer`, or one of `auth.idp.issuer`). |
+| `exp` | Yes | Expired tokens are rejected. |
+| `sub` | Yes | The user's unique ID. |
+| `aud` | Conditional | Enforced in IDP mode when `auth.idp.audience` is non-empty. |
+| `org_name`, `org_handle`, `username`, `email` | No | Identity metadata surfaced to handlers. |
+
+### Claim name mappings
+
+Claim names are configurable, so the same mapping is used for both issuing and validating tokens
+and the two can never drift apart. Each value is either a flat claim name (`org_id`) or a
+dot-separated path into a nested claim (`realm_access.roles`) — useful for IDPs like Keycloak.
+
+```toml
+[platform_api.auth.claim_mappings]
+organization = "organization" # claim carrying the org ID
+org_name = "org_name"
+org_handle = "org_handle"
+user_id = "sub"
+username = "username"
+email = "email"
+scope = "scope" # space-separated scope string
+roles = "" # e.g. "realm_access.roles" (Keycloak) or "roles" (Asgardeo)
+```
+
+## Authorization
+
+### Scope naming convention
+
+Scopes follow `ap:[:]:`:
+
+| Form | Grants |
+|---|---|
+| `ap:rest_api:read` | A single action on a resource |
+| `ap:rest_api:manage` | Every action on that resource (read, create, update, delete) |
+| `ap:rest_api:deployment:manage` | Every action on the **subresource** only — not on the parent `rest_api` |
+
+An operation may declare several acceptable scopes. The token needs **at least one** of them —
+which is why `ap:rest_api:create` and `ap:rest_api:manage` typically both appear on a create
+operation.
+
+### Validation modes
+
+`auth.idp.validation_mode` selects how effective scopes are derived:
+
+- **`scope`** (default) — the token's `scope` claim is used directly.
+- **`role`** — the roles found at `claim_mappings.roles` are expanded into scopes via the YAML file
+ at `auth.idp.role_mappings`. When a token carries several roles, the effective scopes are the
+ **union** of all matching role entries (most permissive wins).
+
+```yaml
+# role_mappings YAML — requires a server restart to take effect
+roles:
+ - name: platform-admin
+ scopes:
+ - ap:organization:manage
+ - ap:rest_api:manage
+ - ap:gateway:manage
+ - name: platform-operator
+ scopes:
+ - ap:organization:read
+ - ap:rest_api:read
+```
+
+### Disabling scope checks
+
+```toml
+[platform_api.auth]
+scope_validation = false
+```
+
+This authenticates callers but performs **no** authorization checks. Use it only to temporarily
+bypass authorization during development — never in a deployed environment.
+
+### Unauthenticated paths
+
+`[platform_api.auth] skip_paths` lists path prefixes that bypass the auth middleware entirely —
+health and metrics probes, the file-mode login endpoint, and the internal gateway routes (which
+authenticate with a gateway token instead). Setting this key **replaces** the built-in default
+list, so keep any entries you still need.
+
+## Error Responses
+
+Authentication and authorization failures are deliberately uniform — the response never reveals
+*why* a credential was rejected. The specific reason is logged internally with a tracking ID.
+
+| Status | Meaning | Cause |
+|---|---|---|
+| 401 | Unauthorized | Missing `Authorization` header, non-Bearer header, malformed/expired/invalid-signature token, wrong issuer, or missing `organization` claim |
+| 403 | Forbidden | Token is valid, but its effective scopes do not satisfy the operation's required scopes |
+
+A `401` carries the identical payload for every one of those causes, so it cannot be used to probe
+which part of a credential was wrong.
+
+## Troubleshooting
+
+- **Every request returns 401 after switching to IDP mode** — check that `jwks_url` is reachable
+ from the Platform API and that the token's `iss` is listed in `auth.idp.issuer`.
+- **401 with a token that the IDP says is valid** — the `organization` claim is probably missing or
+ under a different name. Point `claim_mappings.organization` at the claim your IDP actually emits.
+- **401 only for some clients in IDP mode** — `auth.idp.audience` is non-empty and those tokens were
+ minted for a different client. Add the audience value or leave the list empty to skip the check.
+- **403 on operations the user should be able to perform** — compare the token's `scope` claim
+ against the scopes listed on that operation's page. In `role` mode, confirm the role name in the
+ token matches an entry in the `role_mappings` file (it requires a restart after editing).
+- **403 in `role` mode on every operation** — `claim_mappings.roles` is unset, so no roles are read
+ from the token. Set it to the claim path your IDP uses (`realm_access.roles` for Keycloak,
+ `roles` for Asgardeo/Entra ID).
+- **Login endpoint returns 401 in `file` mode** — the stored `password_hash` must be a bcrypt hash;
+ a plaintext password there will never match.
+
+## Scope Reference
+
+The complete set of scopes defined by this API, as declared in the OpenAPI `OAuth2Security` scheme.
+
+- OAuth2, flow: `clientCredentials`
+- Token URL declared in the spec: `https://localhost:9243/oauth2/token`
+
+The declared token URL describes the `clientCredentials` flow an external IDP is expected to expose,
+and is what generated clients target in `idp` mode — the Platform API does not serve it itself.
+In `file` mode there is no client-credentials endpoint; obtain a token from the username/password
+login endpoint described under [Obtaining a token](#obtaining-a-token)
+(`POST /api/portal/v0.9/auth/login`) instead.
+
+|Scope|Scope Description|
+|---|---|
+|ap:api_key:read|Read API keys owned by the current user|
+|ap:application:api_key:create|Create an application API key|
+|ap:application:api_key:delete|Delete an application API key|
+|ap:application:api_key:manage|Full access to application API keys|
+|ap:application:api_key:read|Read application API keys|
+|ap:application:association:api_key:read|Read API keys for an application association|
+|ap:application:association:create|Create an application association|
+|ap:application:association:delete|Delete an application association|
+|ap:application:association:manage|Full access to application associations|
+|ap:application:association:read|Read application associations|
+|ap:application:create|Create an application|
+|ap:application:delete|Delete an application|
+|ap:application:manage|Full access to applications|
+|ap:application:read|Read applications|
+|ap:application:update|Update an application|
+|ap:gateway:create|Create a gateway|
+|ap:gateway:delete|Delete a gateway|
+|ap:gateway:manage|Full access to gateways|
+|ap:gateway:manifest:read|Read a gateway manifest|
+|ap:gateway:read|Read gateways|
+|ap:gateway_custom_policy:create|Create a gateway custom policy|
+|ap:gateway_custom_policy:delete|Delete a gateway custom policy|
+|ap:gateway_custom_policy:manage|Full access to gateway custom policies|
+|ap:gateway_custom_policy:read|Read gateway custom policies|
+|ap:gateway:token:create|Create a gateway token|
+|ap:gateway:token:delete|Delete a gateway token|
+|ap:gateway:token:manage|Full access to gateway tokens|
+|ap:gateway:token:read|Read gateway tokens|
+|ap:gateway:update|Update a gateway|
+|ap:llm_provider:api_key:create|Create an LLM provider API key|
+|ap:llm_provider:api_key:delete|Delete an LLM provider API key|
+|ap:llm_provider:api_key:manage|Full access to LLM provider API keys|
+|ap:llm_provider:api_key:read|Read LLM provider API keys|
+|ap:llm_provider:create|Create an LLM provider|
+|ap:llm_provider:delete|Delete an LLM provider|
+|ap:llm_provider:deployment:create|Deploy an LLM provider|
+|ap:llm_provider:deployment:delete|Delete an LLM provider deployment|
+|ap:llm_provider:deployment:manage|Full access to LLM provider deployments|
+|ap:llm_provider:deployment:read|Read LLM provider deployments|
+|ap:llm_provider:deployment:restore|Restore an LLM provider deployment|
+|ap:llm_provider:deployment:undeploy|Undeploy an LLM provider deployment|
+|ap:llm_provider:manage|Full access to LLM providers|
+|ap:llm_provider:read|Read LLM providers|
+|ap:llm_provider:update|Update an LLM provider|
+|ap:llm_proxy:api_key:create|Create an LLM proxy API key|
+|ap:llm_proxy:api_key:delete|Delete an LLM proxy API key|
+|ap:llm_proxy:api_key:manage|Full access to LLM proxy API keys|
+|ap:llm_proxy:api_key:read|Read LLM proxy API keys|
+|ap:llm_proxy:create|Create an LLM proxy|
+|ap:llm_proxy:delete|Delete an LLM proxy|
+|ap:llm_proxy:deployment:create|Deploy an LLM proxy|
+|ap:llm_proxy:deployment:delete|Delete an LLM proxy deployment|
+|ap:llm_proxy:deployment:manage|Full access to LLM proxy deployments|
+|ap:llm_proxy:deployment:read|Read LLM proxy deployments|
+|ap:llm_proxy:deployment:restore|Restore an LLM proxy deployment|
+|ap:llm_proxy:deployment:undeploy|Undeploy an LLM proxy deployment|
+|ap:llm_proxy:manage|Full access to LLM proxies|
+|ap:llm_proxy:read|Read LLM proxies|
+|ap:llm_proxy:update|Update an LLM proxy|
+|ap:llm_template:create|Create an LLM provider template|
+|ap:llm_template:delete|Delete an LLM provider template|
+|ap:llm_template:manage|Full access to LLM provider templates|
+|ap:llm_template:read|Read LLM provider templates|
+|ap:llm_template:update|Update an LLM provider template|
+|ap:mcp_proxy:create|Create an MCP proxy|
+|ap:mcp_proxy:delete|Delete an MCP proxy|
+|ap:mcp_proxy:deployment:create|Deploy an MCP proxy|
+|ap:mcp_proxy:deployment:delete|Delete an MCP proxy deployment|
+|ap:mcp_proxy:deployment:manage|Full access to MCP proxy deployments|
+|ap:mcp_proxy:deployment:read|Read MCP proxy deployments|
+|ap:mcp_proxy:deployment:restore|Restore an MCP proxy deployment|
+|ap:mcp_proxy:deployment:undeploy|Undeploy an MCP proxy deployment|
+|ap:mcp_proxy:manage|Full access to MCP proxies|
+|ap:mcp_proxy:read|Read MCP proxies|
+|ap:mcp_proxy:update|Update an MCP proxy|
+|ap:organization:create|Create an organization|
+|ap:organization:manage|Full access to an organization|
+|ap:organization:read|Read an organization|
+|ap:project:create|Create a project|
+|ap:project:delete|Delete a project|
+|ap:project:manage|Full access to projects|
+|ap:project:read|Read projects|
+|ap:project:update|Update a project|
+|ap:rest_api:api_key:create|Create an API key for a REST API|
+|ap:rest_api:api_key:delete|Delete an API key of a REST API|
+|ap:rest_api:api_key:manage|Full access to a REST API's API keys|
+|ap:rest_api:api_key:update|Update an API key of a REST API|
+|ap:rest_api:create|Create a REST API|
+|ap:rest_api:delete|Delete a REST API|
+|ap:rest_api:deployment:create|Deploy a REST API|
+|ap:rest_api:deployment:delete|Delete a REST API deployment|
+|ap:rest_api:deployment:manage|Full access to REST API deployments|
+|ap:rest_api:deployment:read|Read REST API deployments|
+|ap:rest_api:deployment:restore|Restore a REST API deployment|
+|ap:rest_api:deployment:undeploy|Undeploy a REST API deployment|
+|ap:rest_api:gateway:create|Add gateways to a REST API|
+|ap:rest_api:gateway:manage|Full access to a REST API's gateways|
+|ap:rest_api:gateway:read|Read a REST API's gateways|
+|ap:rest_api:manage|Full access to REST APIs|
+|ap:rest_api:read|Read REST APIs|
+|ap:rest_api:update|Update a REST API|
+|ap:secret:create|Create a secret|
+|ap:secret:delete|Delete a secret|
+|ap:secret:manage|Full access to secrets|
+|ap:secret:read|Read secrets|
+|ap:secret:update|Update a secret|
+|ap:subscription:create|Create a subscription|
+|ap:subscription:delete|Delete a subscription|
+|ap:subscription:manage|Full access to subscriptions|
+|ap:subscription:read|Read subscriptions|
+|ap:subscription:update|Update a subscription|
+|ap:subscription_plan:create|Create a subscription plan|
+|ap:subscription_plan:delete|Delete a subscription plan|
+|ap:subscription_plan:manage|Full access to subscription plans|
+|ap:subscription_plan:read|Read subscription plans|
+|ap:subscription_plan:update|Update a subscription plan|
diff --git a/docs/rest-apis/platform-api/gateway-policies.md b/docs/rest-apis/platform-api/gateway-policies.md
new file mode 100644
index 0000000000..efdaef8c12
--- /dev/null
+++ b/docs/rest-apis/platform-api/gateway-policies.md
@@ -0,0 +1,431 @@
+
Gateway Policies
+
+Custom gateway policy management operations
+
+## Get synced custom policies for the current organization
+
+
+
+`GET /gateway-custom-policies`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/gateway-custom-policies \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns all custom policies synced to the current organization (from the JWT `organization` claim).
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 0,
+ "list": [
+ {
+ "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "organizationUuid": "bc554ded-7e40-44a7-b397-48480793ad03",
+ "name": "rate-limit-custom",
+ "version": "1.0.0",
+ "description": "Custom rate limiting policy",
+ "policyDefinition": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of custom policies|[CustomPolicyListResponse](schemas.md#schemacustompolicylistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Sync a custom policy from the gateway manifest
+
+
+
+`POST /gateway-custom-policies/sync`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/gateway-custom-policies/sync?gatewayId=prod-gateway-01&policyName=set-wso2-headers&policyVersion=1.0.0 \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Syncs a custom policy from the gateway manifest into the organization's custom policy registry.
+Version-based rules apply:
+- **New major version**: creates a new policy record (e.g. v1.x.x and v2.x.x coexist).
+- **New minor version** (same major): updates the existing record (e.g. v1.1.0 → v1.2.0).
+- **Patch version change** (same major.minor): not allowed.
+- **Downgrade**: not allowed.
+Policy names are case-insensitive.
+After syncing, the policy can be applied to APIs in the organization.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|gatewayId|query|string|true|Handle (URL-friendly slug) of the gateway whose manifest contains the policy|
+|policyName|query|string|true|Name of the custom policy (case-insensitive)|
+|policyVersion|query|string|true|Version of the custom policy in MAJOR.MINOR.PATCH format|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "organizationUuid": "bc554ded-7e40-44a7-b397-48480793ad03",
+ "name": "rate-limit-custom",
+ "version": "1.0.0",
+ "description": "Custom rate limiting policy",
+ "policyDefinition": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 422 Response
+
+```json
+{
+ "status": "error",
+ "code": "POLICY_INVALID_STATE",
+ "message": "The policy is not a custom policy, or its manifest is unavailable."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Custom policy synced successfully|[CustomPolicyResponse](schemas.md#schemacustompolicyresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|Policy is not a custom policy or manifest is unavailable|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get a specific custom policy version
+
+
+
+`GET /gateway-custom-policies/{gatewayCustomPolicyId}/versions/{version}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/gateway-custom-policies/{gatewayCustomPolicyId}/versions/{version} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns a custom policy by its UUID and version for the current organization.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Custom policy retrieved successfully|[CustomPolicyResponse](schemas.md#schemacustompolicyresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete a specific custom policy version
+
+
+
+`DELETE /gateway-custom-policies/{gatewayCustomPolicyId}/versions/{version}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/gateway-custom-policies/{gatewayCustomPolicyId}/versions/{version} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes a custom policy by its UUID and version. The policy must not be in use by any APIs.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|gatewayCustomPolicyId|path|string(uuid)|true|UUID of the custom policy record|
+|version|path|string|true|Version of the custom policy (e.g. "1.0.0")|
+
+> Example responses
+>
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Custom policy deleted successfully|None|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/gateway-tokens.md b/docs/rest-apis/platform-api/gateway-tokens.md
new file mode 100644
index 0000000000..25284026a5
--- /dev/null
+++ b/docs/rest-apis/platform-api/gateway-tokens.md
@@ -0,0 +1,358 @@
+
Gateway Tokens
+
+Gateway token rotation and revocation operations
+
+## List active gateway tokens
+
+
+
+`GET /gateways/{gatewayId}/tokens`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/gateways/{gatewayId}/tokens \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns all active tokens for the specified gateway. Token hashes and salts are never exposed.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|gatewayId|path|string|true|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 0,
+ "list": [
+ {
+ "id": "abc12345-f678-90de-f123-456789abcdef",
+ "status": "active",
+ "createdAt": "2025-10-14T10:30:00Z",
+ "revokedAt": null
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of active tokens|[GatewayTokenListResponse](schemas.md#schemagatewaytokenlistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Rotate gateway token
+
+
+
+`POST /gateways/{gatewayId}/tokens`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/gateways/{gatewayId}/tokens \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Generates a new authentication token for the gateway. The existing token remains active
+to enable zero-downtime rotation. Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|gatewayId|path|string|true|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.|
+
+#### Detailed descriptions
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.
+
+> Example responses
+>
+> 201 Response
+
+```json
+{
+ "id": "def45678-g901-23hi-j456-789012klmnop",
+ "token": "REDACTED_TOKEN",
+ "createdAt": "2025-10-15T14:20:00Z",
+ "message": "New token generated successfully. Old token remains active until revoked."
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|New token generated successfully|[TokenRotationResponse](schemas.md#schematokenrotationresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Revoke gateway token
+
+
+
+`DELETE /gateways/{gatewayId}/tokens/{tokenId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/gateways/{gatewayId}/tokens/{tokenId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Revokes a specific gateway token. Operation is idempotent - revoking an already-revoked
+token succeeds. Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|gatewayId|path|string|true|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.|
+|tokenId|path|string(uuid)|true|**Token ID** consisting of the **UUID** of the Token.|
+
+#### Detailed descriptions
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.
+
+**tokenId**: **Token ID** consisting of the **UUID** of the Token.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "message": "Token revoked successfully"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Token revoked successfully|Inline|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Gateway registered successfully|[GatewayResponse](schemas.md#schemagatewayresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List all gateways
+
+
+
+`GET /gateways`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/gateways \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves a list of all registered gateways for the organization specified in the JWT token.
+Organization ID is automatically extracted from the token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+|sortBy|query|string|false|Field to sort the collection by. An unrecognized value falls back to the default sort (createdAt).|
+|sortOrder|query|string|false|Sort direction applied to `sortBy`.|
+|query|query|string|false|Case-insensitive substring filter matched against the resource id (handle).|
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|sortBy|name|
+|sortBy|createdAt|
+|sortOrder|asc|
+|sortOrder|desc|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "prod-gateway-01",
+ "organizationId": "acme",
+ "displayName": "Production Gateway 01",
+ "description": "Production gateway for handling API traffic",
+ "properties": {
+ "region": "us-west",
+ "tier": "premium"
+ },
+ "endpoints": [
+ "https://api.example.com:8443/api/v1",
+ "wss://events.example.com:8444"
+ ],
+ "isCritical": true,
+ "functionalityType": "regular",
+ "version": "1.0",
+ "isActive": true,
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2025-10-14T10:30:00Z",
+ "updatedAt": "2025-10-14T10:30:00Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Gateways retrieved successfully|[GatewayListResponse](schemas.md#schemagatewaylistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get gateway by ID
+
+
+
+`GET /gateways/{gatewayId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/gateways/{gatewayId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves a specific gateway by its ID (handle). Access is validated against the organization
+in the JWT token.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Gateway retrieved successfully|[GatewayResponse](schemas.md#schemagatewayresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Update gateway
+
+
+
+`PUT /gateways/{gatewayId}`
+
+> Code samples
+
+```shell
+
+curl -X PUT https://localhost:9243/api/v0.9/gateways/{gatewayId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Updates an existing gateway's mutable fields (description).
+Access is validated against the organization in the JWT token.
+
+> Payload
+
+```json
+{
+ "organizationId": "acme",
+ "displayName": "Production Gateway 01",
+ "description": "Production gateway for handling API traffic",
+ "properties": {
+ "region": "us-west",
+ "tier": "premium"
+ },
+ "endpoints": [
+ "https://api.example.com:8443/api/v1",
+ "wss://events.example.com:8444"
+ ],
+ "isCritical": true,
+ "functionalityType": "regular",
+ "version": "1.0",
+ "isActive": true,
+ "createdAt": "2025-10-14T10:30:00Z",
+ "updatedAt": "2025-10-14T10:30:00Z"
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|gatewayId|path|string|true|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.|
+|body|body|[GatewayResponse](schemas.md#schemagatewayresponse)|true|Gateway object that needs to be updated|
+
+#### Detailed descriptions
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "id": "prod-gateway-01",
+ "organizationId": "acme",
+ "displayName": "Production Gateway 01",
+ "description": "Production gateway for handling API traffic",
+ "properties": {
+ "region": "us-west",
+ "tier": "premium"
+ },
+ "endpoints": [
+ "https://api.example.com:8443/api/v1",
+ "wss://events.example.com:8444"
+ ],
+ "isCritical": true,
+ "functionalityType": "regular",
+ "version": "1.0",
+ "isActive": true,
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2025-10-14T10:30:00Z",
+ "updatedAt": "2025-10-14T10:30:00Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Gateway updated successfully|[GatewayResponse](schemas.md#schemagatewayresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete gateway
+
+
+
+`DELETE /gateways/{gatewayId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/gateways/{gatewayId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Permanently deletes a gateway and all associated tokens (CASCADE).
+Deletion is blocked if the gateway has active API deployments or WebSocket connections.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|gatewayId|path|string|true|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.|
+
+#### Detailed descriptions
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Gateway deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get gateway policy manifest
+
+
+
+`GET /gateways/{gatewayId}/manifest`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/gateways/{gatewayId}/manifest \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns the policy manifest for the specified gateway. The manifest is populated by the gateway
+controller when it connects to the platform API, and contains all installed policies. Custom
+policies additionally include their full policy definition schema.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|gatewayId|path|string|true|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.|
+
+#### Detailed descriptions
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "policies": [
+ {
+ "name": "set-wso2-headers",
+ "version": "v0.8.0",
+ "description": "Sets WSO2-specific headers in the request and response.",
+ "isCustomPolicy": true,
+ "policyDefinition": {}
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Gateway policy manifest|[ManifestSyncResponse](schemas.md#schemamanifestsyncresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/llm-provider-deployments.md b/docs/rest-apis/platform-api/llm-provider-deployments.md
new file mode 100644
index 0000000000..ea90b19793
--- /dev/null
+++ b/docs/rest-apis/platform-api/llm-provider-deployments.md
@@ -0,0 +1,764 @@
+
LLM Provider Deployments
+
+LLM provider deployment operations
+
+## Create and deploy a new LLM provider deployment
+
+
+
+`POST /llm-providers/{llmProviderId}/deployments`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/deployments \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Creates an immutable deployment artifact for an LLM provider and deploys it to a specified gateway.
+Each deployment targets a single gateway. The providerId parameter is the LLM provider handle (identifier),
+not the UUID. The operation returns a transitional DEPLOYING status. Final success or failure will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+Access is validated against the organization in the JWT token.
+
+> Payload
+
+```json
+{
+ "name": "v1.0-production",
+ "base": "current",
+ "gatewayId": "prod-gateway-01",
+ "metadata": {}
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderId|path|string|true|Unique identifier of the LLM provider|
+|body|body|[DeployRequest](schemas.md#schemadeployrequest)|true|Deployment request with gateway ID, base reference, and metadata|
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes DEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|LLM provider deployed successfully|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Get deployments for an LLM provider
+
+
+
+`GET /llm-providers/{llmProviderId}/deployments`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/deployments \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves all deployment artifacts for a specific LLM provider. The providerId parameter is the
+LLM provider handle (identifier), not the UUID. Supports filtering by gateway handle and deployment status.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderId|path|string|true|Unique identifier of the LLM provider|
+|gatewayId|query|string|false|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway to filter status by.|
+|status|query|string|false|Filter deployments by status (DEPLOYED, UNDEPLOYED, DEPLOYING, UNDEPLOYING, FAILED, or ARCHIVED)|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway to filter status by.
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|status|DEPLOYED|
+|status|UNDEPLOYED|
+|status|DEPLOYING|
+|status|UNDEPLOYING|
+|status|FAILED|
+|status|ARCHIVED|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 0,
+ "list": [
+ {
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYED",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "statusReason": "string",
+ "updatedAt": "2019-08-24T14:15:22Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Deployments retrieved successfully|[DeploymentListResponse](schemas.md#schemadeploymentlistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get LLM provider deployment by ID
+
+
+
+`GET /llm-providers/{llmProviderId}/deployments/{deploymentId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/deployments/{deploymentId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves metadata for a specific LLM provider deployment artifact including status, gateway association,
+and timestamps. Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Deployment metadata retrieved successfully|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete LLM provider deployment
+
+
+
+`DELETE /llm-providers/{llmProviderId}/deployments/{deploymentId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/deployments/{deploymentId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes a deployment artifact. Deletion is only allowed when the deployment is in UNDEPLOYED status.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderId|path|string|true|Unique identifier of the LLM provider|
+|deploymentId|path|string(uuid)|true|The UUID of the deployment|
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "DEPLOYMENT_ACTIVE",
+ "message": "Cannot delete an active deployment - undeploy it first."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Deployment deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The deployment is still active and must be undeployed before deletion.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Undeploy LLM provider deployment from gateway
+
+
+
+`POST /llm-providers/{llmProviderId}/deployments/{deploymentId}/undeploy`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/deployments/{deploymentId}/undeploy?gatewayId=string \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Undeploys an active LLM provider deployment, stopping it from being served on the specified gateway.
+The deployment artifact remains in the system and can be restored later.
+Returns the updated deployment object with initial status UNDEPLOYING. Final status (UNDEPLOYED or FAILED) will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+
+The gatewayId query parameter is validated against the deployment's bound gateway to prevent unintended operations.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderId|path|string|true|Unique identifier of the LLM provider|
+|deploymentId|path|string|true|UUID of the deployment to undeploy|
+|gatewayId|query|string|true|Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)|
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes UNDEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "UNDEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Undeploy initiated successfully. Returns the deployment with initial status UNDEPLOYING. Poll status for final result.|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Restore a previous LLM provider deployment
+
+
+
+`POST /llm-providers/{llmProviderId}/deployments/{deploymentId}/restore`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/deployments/{deploymentId}/restore?gatewayId=string \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Initiates restoring a previous deployment (ARCHIVED or UNDEPLOYED) on the specified gateway.
+Returns the deployment with initial status DEPLOYING. Final success or failure will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+The target deployment must not already be in DEPLOYED status.
+
+The gatewayId query parameter is validated against the deployment's bound gateway to prevent unintended operations.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderId|path|string|true|Unique identifier of the LLM provider|
+|deploymentId|path|string|true|UUID of the deployment to restore (must be ARCHIVED or UNDEPLOYED)|
+|gatewayId|query|string|true|Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)|
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes DEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Restore initiated successfully. Returns the deployment with initial status DEPLOYING. Poll status for final result.|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/llm-provider-templates.md b/docs/rest-apis/platform-api/llm-provider-templates.md
new file mode 100644
index 0000000000..ce41b77e41
--- /dev/null
+++ b/docs/rest-apis/platform-api/llm-provider-templates.md
@@ -0,0 +1,1478 @@
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|LLM provider template created successfully|[LLMProviderTemplate](schemas.md#schemallmprovidertemplate)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List templates, list a family's versions, or get a single version
+
+
+
+`GET /llm-provider-templates`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-provider-templates \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves LLM provider templates based on the `query` parameter:
+ - no `query` -> list all template versions
+ - `query=latest:true` -> list the latest version of each template
+ - `query=groupId:` -> list all versions for the template
+ - `query=groupId:&version:` -> retrieve a specific template version
+
+All filters are provided via the URL-encoded `query` parameter (e.g.
+`?query=groupId%3Awso2-openai%26version%3Av2.0`).
+
+Returns a single `LLMProviderTemplate` only when both `groupId` and `version` are specified;
+otherwise returns an `LLMProviderTemplateListResponse`.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|query|query|string|false|URL-encoded search DSL. `query=latest:true` lists only the latest version of each family; `query=groupId:` lists that family's versions; adding `&version:` returns the single full template for that version. Terms are `&`-separated `key:value` pairs and the whole value is percent-encoded (e.g. groupId%3Awso2-openai%26version%3Av2.0).|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 5,
+ "list": [
+ {
+ "id": "openai",
+ "groupId": "openai",
+ "displayName": "OpenAI",
+ "managedBy": "wso2",
+ "description": "Default OpenAI template",
+ "createdBy": "john.doe",
+ "version": "v1.0",
+ "isLatest": true,
+ "enabled": true,
+ "logoUrl": "https://cdn.example.com/logos/openai.svg",
+ "createdAt": "2025-11-25T10:30:00Z",
+ "updatedAt": "2025-11-25T10:30:00Z",
+ "readOnly": false
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Without a `version` term: a list response — either templates (all versions by default, or the latest of each family when `query=latest:true`) or a family's versions. With `query=groupId:&version:`: the single full template for that version.|Inline|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+
Response Schema
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+|location|payload|
+|location|header|
+|location|queryParam|
+|location|pathParam|
+
+## Create a new version by copying an existing one
+
+
+
+`POST /llm-provider-templates/copy`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/llm-provider-templates/copy?fromTemplateId=openai-v3-0&toVersion=v4.0 \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Creates a new version within a template family by cloning an existing
+version (`fromTemplateId`) and applying optional field overrides from
+the body. The new version lands in the same family as the source and
+becomes the latest.
+
+Versions start at v1.0 and only go higher — `toVersion` must match the
+`v.` pattern with a major of at least 1 (e.g. v2.0); v0.x
+is rejected. `toTemplateId`, when supplied, must equal the handle
+derived from the family and `toVersion`. Organization is identified via
+the JWT token.
+
+Built-in (WSO2-managed) template families are immutable: adding a version
+to one is rejected with `403`. To base a custom template on a built-in,
+create a new template (`POST /llm-provider-templates`) instead — it gets
+its own `group_id` and starts at v1.0.
+
+> Payload
+
+```json
+{
+ "displayName": "OpenAI",
+ "version": "v2.0",
+ "managedBy": "organization",
+ "description": "Default OpenAI template",
+ "openapi": "string",
+ "metadata": {
+ "endpointUrl": "https://api.openai.com",
+ "auth": {
+ "type": "bearer",
+ "header": "Authorization",
+ "valuePrefix": "Bearer "
+ },
+ "logoUrl": "https://cdn.example.com/logos/openai.svg",
+ "openapiSpecUrl": "https://api.openai.com/openapi.json"
+ },
+ "promptTokens": {
+ "location": "payload",
+ "identifier": "$.usage.inputTokens"
+ },
+ "completionTokens": {
+ "location": "payload",
+ "identifier": "$.usage.outputTokens"
+ },
+ "totalTokens": {
+ "location": "payload",
+ "identifier": "$.usage.totalTokens"
+ },
+ "remainingTokens": {
+ "location": "header",
+ "identifier": "x-ratelimit-remaining-tokens"
+ },
+ "requestModel": {
+ "location": "payload",
+ "identifier": "$.model"
+ },
+ "responseModel": {
+ "location": "payload",
+ "identifier": "$.model"
+ },
+ "resourceMappings": {
+ "resources": [
+ {
+ "resource": "/responses",
+ "promptTokens": {
+ "location": "payload",
+ "identifier": "$.usage.inputTokens"
+ },
+ "completionTokens": {
+ "location": "payload",
+ "identifier": "$.usage.outputTokens"
+ },
+ "totalTokens": {
+ "location": "payload",
+ "identifier": "$.usage.totalTokens"
+ },
+ "remainingTokens": {
+ "location": "header",
+ "identifier": "x-ratelimit-remaining-tokens"
+ },
+ "requestModel": {
+ "location": "payload",
+ "identifier": "$.model"
+ },
+ "responseModel": {
+ "location": "payload",
+ "identifier": "$.model"
+ }
+ }
+ ]
+ }
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|fromTemplateId|query|string|true|Handle (id) of the source version to copy from.|
+|toTemplateId|query|string|false|Expected handle of the new version. Must equal the handle derived from the source family and toVersion; used to validate the target.|
+|toVersion|query|string|true|New version identifier, e.g. v4.0. Must match v. with a major of at least 1, and be unique within the family.|
+|body|body|[CreateLLMProviderTemplateVersionRequest](schemas.md#schemacreatellmprovidertemplateversionrequest)|false|Optional overrides applied on top of the copied config. Any field present replaces the value cloned from the source version.|
+
+> Example responses
+>
+> New version created successfully
+
+```json
+{
+ "id": "openai-v4-0",
+ "groupId": "openai",
+ "displayName": "OpenAI",
+ "managedBy": "organization",
+ "description": "Default OpenAI template",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "readOnly": false,
+ "version": "v4.0",
+ "isLatest": true,
+ "enabled": true
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|New version created successfully|[LLMProviderTemplate](schemas.md#schemallmprovidertemplate)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Get LLM provider template by id
+
+
+
+`GET /llm-provider-templates/{llmProviderTemplateId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-provider-templates/{llmProviderTemplateId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieve the complete configuration for a specific LLM provider template.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|LLM provider template updated successfully|[LLMProviderTemplate](schemas.md#schemallmprovidertemplate)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Enable or disable a template version
+
+
+
+`PATCH /llm-provider-templates/{llmProviderTemplateId}`
+
+> Code samples
+
+```shell
+
+curl -X PATCH https://localhost:9243/api/v0.9/llm-provider-templates/{llmProviderTemplateId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Enable or disable the single template version identified by its handle
+(id). Only built-in templates can be toggled; disabling a version in use
+by a provider is rejected (409).
+
+> Payload
+
+```json
+{
+ "enabled": true
+}
+```
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Updated template version|[LLMProviderTemplate](schemas.md#schemallmprovidertemplate)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete a template version
+
+
+
+`DELETE /llm-provider-templates/{llmProviderTemplateId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/llm-provider-templates/{llmProviderTemplateId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Delete the single template version identified by its handle (id).
+Built-in versions are read-only (403); a version still in use by a
+provider is blocked (409).
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderTemplateId|path|string|true|Unique handle of the template version.|
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Version deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/llm-providers.md b/docs/rest-apis/platform-api/llm-providers.md
new file mode 100644
index 0000000000..53cd79a143
--- /dev/null
+++ b/docs/rest-apis/platform-api/llm-providers.md
@@ -0,0 +1,2008 @@
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|LLM provider created successfully|[LLMProvider](schemas.md#schemallmprovider)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List all LLM providers
+
+
+
+`GET /llm-providers`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-providers \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieve a list of all LLM providers.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "wso2-openai-provider",
+ "displayName": "WSO2 OpenAI Provider",
+ "description": "Primary OpenAI provider",
+ "createdBy": "john.doe",
+ "readOnly": false,
+ "version": "v1.0",
+ "template": "openai",
+ "status": "deployed",
+ "createdAt": "2025-11-25T10:30:00Z",
+ "updatedAt": "2025-11-25T10:30:00Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of LLM providers|[LLMProviderListResponse](schemas.md#schemallmproviderlistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get LLM provider by identifier
+
+
+
+`GET /llm-providers/{llmProviderId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-providers/{llmProviderId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieve the complete configuration for a specific LLM provider.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|LLM provider updated successfully|[LLMProvider](schemas.md#schemallmprovider)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete an LLM provider
+
+
+
+`DELETE /llm-providers/{llmProviderId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/llm-providers/{llmProviderId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Remove an LLM provider.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderId|path|string|true|Unique identifier of the LLM provider|
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|LLM provider deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## List LLM proxies by provider
+
+
+
+`GET /llm-providers/{llmProviderId}/llm-proxies`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/llm-proxies \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieve a list of LLM proxies that use the specified LLM provider.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderId|path|string|true|Unique identifier of the LLM provider|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "wso2-con-assistant",
+ "displayName": "WSO2 Con Assistant",
+ "description": "Customer support assistant",
+ "createdBy": "john.doe",
+ "context": "/wso2-con-assistant",
+ "version": "v1.0",
+ "projectId": "default-project",
+ "provider": "wso2-openai-provider",
+ "status": "deployed",
+ "createdAt": "2025-11-25T10:30:00Z",
+ "updatedAt": "2025-11-25T10:30:00Z",
+ "readOnly": false
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of LLM proxies|[LLMProxyListResponse](schemas.md#schemallmproxylistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Create a new API key for an LLM provider
+
+
+
+`POST /llm-providers/{llmProviderId}/api-keys`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/api-keys \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Generates a new API key for the specified LLM provider. The generated key
+is broadcasted to all gateways in the organization and can be used to
+authenticate requests to the LLM provider when API key validation is enabled.
+
+> Payload
+
+```json
+{
+ "id": "production-key",
+ "displayName": "Production Key",
+ "expiresAt": "2026-12-31T23:59:59Z",
+ "issuer": "api-platform-devportal",
+ "allowedTargets": "dev_gateway,test_gateway"
+}
+```
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|API key created successfully|[CreateLLMProviderAPIKeyResponse](schemas.md#schemacreatellmproviderapikeyresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. No gateway connections are currently available to service this request.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List API keys for an LLM provider
+
+
+
+`GET /llm-providers/{llmProviderId}/api-keys`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/api-keys \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns all API keys associated with the specified LLM provider. The plain key value is never returned.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderId|path|string|true|Unique identifier of the LLM provider|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "list": [
+ {
+ "id": "string",
+ "displayName": "string",
+ "maskedApiKey": "string",
+ "status": "active",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "createdBy": "john.doe",
+ "updatedAt": "2019-08-24T14:15:22Z",
+ "expiresAt": "2019-08-24T14:15:22Z",
+ "issuer": "api-platform-devportal",
+ "allowedTargets": "string"
+ }
+ ],
+ "count": 0,
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of API keys retrieved successfully|[LLMProviderAPIKeyListResponse](schemas.md#schemallmproviderapikeylistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete an API key for an LLM provider
+
+
+
+`DELETE /llm-providers/{llmProviderId}/api-keys/{apiKeyId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/llm-providers/{llmProviderId}/api-keys/{apiKeyId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes the key from the database and broadcasts a revoke event to the allowed gateways.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProviderId|path|string|true|Unique identifier of the LLM provider|
+|apiKeyId|path|string|true|Name of the API key to delete|
+
+> Example responses
+>
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+> 503 Response
+
+```json
+{
+ "status": "error",
+ "code": "GATEWAY_CONNECTION_UNAVAILABLE",
+ "message": "No gateway connections are currently available.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|API key deleted successfully|None|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. No gateway connections are currently available to service this request.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/llm-proxies.md b/docs/rest-apis/platform-api/llm-proxies.md
new file mode 100644
index 0000000000..870bfd7d98
--- /dev/null
+++ b/docs/rest-apis/platform-api/llm-proxies.md
@@ -0,0 +1,1315 @@
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|LLM proxy created successfully|[LLMProxy](schemas.md#schemallmproxy)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List all LLM proxies
+
+
+
+`GET /llm-proxies`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-proxies?projectId=default-project \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieve a list of all LLM proxies for a project. Requires the projectId query parameter.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|projectId|query|string|true|**Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**projectId**: **Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "wso2-con-assistant",
+ "displayName": "WSO2 Con Assistant",
+ "description": "Customer support assistant",
+ "createdBy": "john.doe",
+ "context": "/wso2-con-assistant",
+ "version": "v1.0",
+ "projectId": "default-project",
+ "provider": "wso2-openai-provider",
+ "status": "deployed",
+ "createdAt": "2025-11-25T10:30:00Z",
+ "updatedAt": "2025-11-25T10:30:00Z",
+ "readOnly": false
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of LLM proxies|[LLMProxyListResponse](schemas.md#schemallmproxylistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get LLM proxy by unique identifier
+
+
+
+`GET /llm-proxies/{llmProxyId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieve the complete configuration for a specific LLM proxy.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|LLM proxy updated successfully|[LLMProxy](schemas.md#schemallmproxy)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete an LLM proxy
+
+
+
+`DELETE /llm-proxies/{llmProxyId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Remove an LLM proxy.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProxyId|path|string|true|Unique identifier of the LLM proxy|
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|LLM proxy deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Create a new API key for an LLM proxy
+
+
+
+`POST /llm-proxies/{llmProxyId}/api-keys`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId}/api-keys \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Generates a new API key for the specified LLM proxy. The generated key
+is broadcasted to all gateways in the organization and can be used to
+authenticate requests to the LLM proxy when API key validation is enabled.
+
+> Payload
+
+```json
+{
+ "id": "production-key",
+ "displayName": "Production Key",
+ "expiresAt": "2026-12-31T23:59:59Z",
+ "issuer": "api-platform-devportal",
+ "allowedTargets": "dev_gateway,test_gateway"
+}
+```
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|API key created successfully|[CreateLLMProxyAPIKeyResponse](schemas.md#schemacreatellmproxyapikeyresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. No gateway connections are currently available to service this request.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List API keys for an LLM proxy
+
+
+
+`GET /llm-proxies/{llmProxyId}/api-keys`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId}/api-keys \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns all API keys associated with the specified LLM proxy. The plain key value is never returned.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProxyId|path|string|true|Unique identifier of the LLM proxy|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "list": [
+ {
+ "id": "string",
+ "displayName": "string",
+ "maskedApiKey": "string",
+ "status": "active",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "createdBy": "john.doe",
+ "updatedAt": "2019-08-24T14:15:22Z",
+ "expiresAt": "2019-08-24T14:15:22Z",
+ "issuer": "api-platform-devportal",
+ "allowedTargets": "string"
+ }
+ ],
+ "count": 0,
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of API keys retrieved successfully|[LLMProxyAPIKeyListResponse](schemas.md#schemallmproxyapikeylistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete an API key for an LLM proxy
+
+
+
+`DELETE /llm-proxies/{llmProxyId}/api-keys/{apiKeyId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId}/api-keys/{apiKeyId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes the key from the database and broadcasts a revoke event to the allowed gateways.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProxyId|path|string|true|Unique identifier of the LLM proxy|
+|apiKeyId|path|string|true|Name of the API key to delete|
+
+> Example responses
+>
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+> 503 Response
+
+```json
+{
+ "status": "error",
+ "code": "GATEWAY_CONNECTION_UNAVAILABLE",
+ "message": "No gateway connections are currently available.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|API key deleted successfully|None|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. No gateway connections are currently available to service this request.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/llm-proxy-deployments.md b/docs/rest-apis/platform-api/llm-proxy-deployments.md
new file mode 100644
index 0000000000..57071e004e
--- /dev/null
+++ b/docs/rest-apis/platform-api/llm-proxy-deployments.md
@@ -0,0 +1,764 @@
+
LLM Proxy Deployments
+
+LLM proxy deployment operations
+
+## Create and deploy a new LLM proxy deployment
+
+
+
+`POST /llm-proxies/{llmProxyId}/deployments`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId}/deployments \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Creates an immutable deployment artifact for an LLM proxy and deploys it to a specified gateway.
+Each deployment targets a single gateway. The proxyId parameter is the LLM proxy handle (identifier),
+not the UUID. The operation returns a transitional DEPLOYING status. Final success or failure will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+Access is validated against the organization in the JWT token.
+
+> Payload
+
+```json
+{
+ "name": "v1.0-production",
+ "base": "current",
+ "gatewayId": "prod-gateway-01",
+ "metadata": {}
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProxyId|path|string|true|Unique identifier of the LLM proxy|
+|body|body|[DeployRequest](schemas.md#schemadeployrequest)|true|Deployment request with gateway ID, base reference, and metadata|
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes DEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|LLM proxy deployed successfully|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Get deployments for an LLM proxy
+
+
+
+`GET /llm-proxies/{llmProxyId}/deployments`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId}/deployments \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves all deployment artifacts for a specific LLM proxy. The proxyId parameter is the
+LLM proxy handle (identifier), not the UUID. Supports filtering by gateway handle and deployment status.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProxyId|path|string|true|Unique identifier of the LLM proxy|
+|gatewayId|query|string|false|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway to filter status by.|
+|status|query|string|false|Filter deployments by status (DEPLOYED, UNDEPLOYED, DEPLOYING, UNDEPLOYING, FAILED, or ARCHIVED)|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway to filter status by.
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|status|DEPLOYED|
+|status|UNDEPLOYED|
+|status|DEPLOYING|
+|status|UNDEPLOYING|
+|status|FAILED|
+|status|ARCHIVED|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 0,
+ "list": [
+ {
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYED",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "statusReason": "string",
+ "updatedAt": "2019-08-24T14:15:22Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Deployments retrieved successfully|[DeploymentListResponse](schemas.md#schemadeploymentlistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get LLM proxy deployment by ID
+
+
+
+`GET /llm-proxies/{llmProxyId}/deployments/{deploymentId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId}/deployments/{deploymentId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves metadata for a specific LLM proxy deployment artifact including status, gateway association,
+and timestamps. Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Deployment metadata retrieved successfully|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete LLM proxy deployment
+
+
+
+`DELETE /llm-proxies/{llmProxyId}/deployments/{deploymentId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId}/deployments/{deploymentId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes a deployment artifact. Deletion is only allowed when the deployment is in UNDEPLOYED status.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProxyId|path|string|true|Unique identifier of the LLM proxy|
+|deploymentId|path|string(uuid)|true|The UUID of the deployment|
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "DEPLOYMENT_ACTIVE",
+ "message": "Cannot delete an active deployment - undeploy it first."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Deployment deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The deployment is still active and must be undeployed before deletion.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Undeploy LLM proxy deployment from gateway
+
+
+
+`POST /llm-proxies/{llmProxyId}/deployments/{deploymentId}/undeploy`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId}/deployments/{deploymentId}/undeploy?gatewayId=string \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Undeploys an active LLM proxy deployment, stopping it from being served on the specified gateway.
+The deployment artifact remains in the system and can be restored later.
+Returns the updated deployment object with initial status UNDEPLOYING. Final status (UNDEPLOYED or FAILED) will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+
+The gatewayId query parameter is validated against the deployment's bound gateway to prevent unintended operations.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProxyId|path|string|true|Unique identifier of the LLM proxy|
+|deploymentId|path|string|true|UUID of the deployment to undeploy|
+|gatewayId|query|string|true|Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)|
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes UNDEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "UNDEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Undeploy initiated successfully. Returns the deployment with initial status UNDEPLOYING. Poll status for final result.|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Restore a previous LLM proxy deployment
+
+
+
+`POST /llm-proxies/{llmProxyId}/deployments/{deploymentId}/restore`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/llm-proxies/{llmProxyId}/deployments/{deploymentId}/restore?gatewayId=string \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Initiates restoring a previous deployment (ARCHIVED or UNDEPLOYED) on the specified gateway.
+Returns the deployment with initial status DEPLOYING. Final success or failure will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+The target deployment must not already be in DEPLOYED status.
+
+The gatewayId query parameter is validated against the deployment's bound gateway to prevent unintended operations.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|llmProxyId|path|string|true|Unique identifier of the LLM proxy|
+|deploymentId|path|string|true|UUID of the deployment to restore (must be ARCHIVED or UNDEPLOYED)|
+|gatewayId|query|string|true|Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)|
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes DEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Restore initiated successfully. Returns the deployment with initial status DEPLOYING. Poll status for final result.|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/mcp-proxies.md b/docs/rest-apis/platform-api/mcp-proxies.md
new file mode 100644
index 0000000000..09fd41bcf0
--- /dev/null
+++ b/docs/rest-apis/platform-api/mcp-proxies.md
@@ -0,0 +1,958 @@
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|MCP proxy created successfully|[MCPProxy](schemas.md#schemamcpproxy)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List all MCP proxies
+
+
+
+`GET /mcp-proxies`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/mcp-proxies?projectId=default-project \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieve a list of all MCP proxies for a project. Requires the projectId query parameter.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|projectId|query|string|true|**Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**projectId**: **Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "weather-mcp-proxy",
+ "displayName": "Weather Server",
+ "description": "An MCP server which provides weather information",
+ "createdBy": "john.doe",
+ "context": "/weather-mcp-proxy",
+ "version": "v1.0",
+ "projectId": "default-project",
+ "status": "deployed",
+ "mcpSpecVersion": "2025-11-25",
+ "createdAt": "2025-11-25T10:30:00Z",
+ "updatedAt": "2025-11-25T10:30:00Z",
+ "readOnly": false
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of MCP proxies|[MCPProxyListResponse](schemas.md#schemamcpproxylistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get MCP proxy by unique identifier
+
+
+
+`GET /mcp-proxies/{mcpProxyId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/mcp-proxies/{mcpProxyId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieve the complete configuration for a specific MCP proxy.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|MCP proxy updated successfully|[MCPProxy](schemas.md#schemamcpproxy)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete an MCP proxy
+
+
+
+`DELETE /mcp-proxies/{mcpProxyId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/mcp-proxies/{mcpProxyId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Remove an MCP proxy.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|mcpProxyId|path|string|true|Unique identifier of the MCP proxy|
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|MCP proxy deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Fetch server info from MCP proxy backend services
+
+
+
+`POST /mcp-proxies/fetch-server-info`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/mcp-proxies/fetch-server-info \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Fetches server information from the backend services of an MCP proxy.
+This is used to validate connectivity and retrieve metadata about the backend services.
+
+> Payload
+
+```json
+{
+ "url": "https://mcp.server.com/mcp",
+ "proxyId": "my-mcp-proxy",
+ "auth": {
+ "type": "api-key",
+ "header": "X-API-Key",
+ "value": "my-api-key-value"
+ }
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|body|body|[MCPServerInfoFetchRequest](schemas.md#schemamcpserverinfofetchrequest)|true|Target MCP server to introspect — either a direct `url` (with optional `auth`), or a `proxyId` to refetch using a stored proxy configuration.|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "serverInfo": {},
+ "tools": [
+ {}
+ ],
+ "resources": [
+ {}
+ ],
+ "prompts": [
+ {}
+ ]
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Server info retrieved successfully|[MCPServerInfoFetchResponse](schemas.md#schemamcpserverinfofetchresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/mcp-proxy-deployments.md b/docs/rest-apis/platform-api/mcp-proxy-deployments.md
new file mode 100644
index 0000000000..4043292cf4
--- /dev/null
+++ b/docs/rest-apis/platform-api/mcp-proxy-deployments.md
@@ -0,0 +1,720 @@
+
MCP Proxy Deployments
+
+MCP proxy deployment operations
+
+## Create and deploy a new deployment for MCP proxy
+
+
+
+`POST /mcp-proxies/{mcpProxyId}/deployments`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/mcp-proxies/{mcpProxyId}/deployments \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Creates an immutable deployment artifact for an MCP proxy and deploys it to a specified gateway.
+Each deployment targets a single gateway. The id parameter is the MCP proxy handle (identifier),
+not the UUID. The operation returns a transitional DEPLOYING status. Final success or failure will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+Access is validated against the organization in the JWT token.
+
+> Payload
+
+```json
+{
+ "name": "v1.0-production",
+ "base": "current",
+ "gatewayId": "prod-gateway-01",
+ "metadata": {}
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|mcpProxyId|path|string|true|Unique identifier of the MCP proxy|
+|body|body|[DeployRequest](schemas.md#schemadeployrequest)|true|Deployment request with gateway ID, base reference, and metadata|
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes DEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|MCP proxy deployed successfully|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Get deployments for an MCP proxy
+
+
+
+`GET /mcp-proxies/{mcpProxyId}/deployments`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/mcp-proxies/{mcpProxyId}/deployments \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves all deployment artifacts for a specific MCP proxy. The id parameter is the MCP proxy handle (identifier),
+not the UUID. Supports filtering by gateway handle and deployment status.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|mcpProxyId|path|string|true|Unique identifier of the MCP proxy|
+|gatewayId|query|string|false|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway to filter status by.|
+|status|query|string|false|Filter deployments by status (DEPLOYED, UNDEPLOYED, DEPLOYING, UNDEPLOYING, FAILED, or ARCHIVED)|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway to filter status by.
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|status|DEPLOYED|
+|status|UNDEPLOYED|
+|status|DEPLOYING|
+|status|UNDEPLOYING|
+|status|FAILED|
+|status|ARCHIVED|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 0,
+ "list": [
+ {
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYED",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "statusReason": "string",
+ "updatedAt": "2019-08-24T14:15:22Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Deployments retrieved successfully|[DeploymentListResponse](schemas.md#schemadeploymentlistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get deployment by ID
+
+
+
+`GET /mcp-proxies/{mcpProxyId}/deployments/{deploymentId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/mcp-proxies/{mcpProxyId}/deployments/{deploymentId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves metadata for a specific deployment artifact including status, gateway association,
+and timestamps. Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Deployment metadata retrieved successfully|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete deployment
+
+
+
+`DELETE /mcp-proxies/{mcpProxyId}/deployments/{deploymentId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/mcp-proxies/{mcpProxyId}/deployments/{deploymentId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes a deployment artifact. Deletion is only allowed when the deployment is in UNDEPLOYED status.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|mcpProxyId|path|string|true|Unique identifier of the MCP proxy|
+|deploymentId|path|string(uuid)|true|The UUID of the deployment|
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "DEPLOYMENT_ACTIVE",
+ "message": "Cannot delete an active deployment - undeploy it first."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Deployment deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The deployment is still active and must be undeployed before deletion.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Undeploy deployment from gateway
+
+
+
+`POST /mcp-proxies/{mcpProxyId}/deployments/{deploymentId}/undeploy`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}/undeploy?gatewayId=string \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Undeploys an active deployment, stopping the MCP proxy from being served on the specified gateway.
+The deployment artifact remains in the system and can be restored later.
+Returns the updated deployment object with initial status UNDEPLOYING. Final status (UNDEPLOYED or FAILED) will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+
+The gatewayId query parameter is validated against the deployment's bound gateway to prevent unintended operations.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|mcpProxyId|path|string|true|Unique identifier of the MCP proxy|
+|deploymentId|path|string|true|UUID of the deployment to undeploy|
+|gatewayId|query|string|true|Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)|
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes UNDEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "UNDEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Undeploy initiated successfully. Returns the deployment with initial status UNDEPLOYING. Poll status for final result.|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Restore a previous deployment
+
+
+
+`POST /mcp-proxies/{mcpProxyId}/deployments/{deploymentId}/restore`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}/restore?gatewayId=string \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Initiates restoring a previous deployment (ARCHIVED or UNDEPLOYED) on the specified gateway.
+Returns the deployment with initial status DEPLOYING. Final success or failure will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+The target deployment must not already be in DEPLOYED status.
+
+The gatewayId query parameter is validated against the deployment's bound gateway to prevent unintended operations.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|mcpProxyId|path|string|true|Unique identifier of the MCP proxy|
+|deploymentId|path|string|true|UUID of the deployment to restore (must be ARCHIVED or UNDEPLOYED)|
+|gatewayId|query|string|true|Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)|
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes DEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Restore initiated successfully. Returns the deployment with initial status DEPLOYING. Poll status for final result.|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/organizations.md b/docs/rest-apis/platform-api/organizations.md
new file mode 100644
index 0000000000..71e172c723
--- /dev/null
+++ b/docs/rest-apis/platform-api/organizations.md
@@ -0,0 +1,372 @@
+
Organizations
+
+Organization management operations
+
+## Register a new organization
+
+
+
+`POST /organizations`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/organizations \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Registers a new organization with a unique UUID and handle.
+This endpoint is used during the organization onboarding process.
+A valid Bearer JWT token with the `ap:organization:create` or `ap:organization:manage` scope is required.
+
+> Payload
+
+```json
+{
+ "displayName": "Acme Corporation",
+ "region": "us"
+}
+```
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Organization registered successfully|[Organization](schemas.md#schemaorganization)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List organizations
+
+
+
+`GET /organizations`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/organizations \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves the list of organizations. Results are returned in a paginated
+collection envelope.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "acme",
+ "displayName": "Acme Corporation",
+ "region": "us",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Organizations retrieved successfully|[OrganizationListResponse](schemas.md#schemaorganizationlistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get organization by ID
+
+
+
+`GET /organizations/{organizationId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/organizations/{organizationId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns the organization with the specified handle (id).
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Organization retrieved successfully|[Organization](schemas.md#schemaorganization)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Check if organization exists
+
+
+
+`HEAD /organizations/{organizationId}`
+
+> Code samples
+
+```shell
+
+curl -X HEAD https://localhost:9243/api/v0.9/organizations/{organizationId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Checks if an organization with the specified handle (id) exists.
+Returns only headers without a response body.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|organizationId|path|string|true|ID of the organization.|
+
+> Example responses
+>
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Organization exists|None|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/projects.md b/docs/rest-apis/platform-api/projects.md
new file mode 100644
index 0000000000..9165633945
--- /dev/null
+++ b/docs/rest-apis/platform-api/projects.md
@@ -0,0 +1,620 @@
+
Projects
+
+Project management operations
+
+## Create a new project
+
+
+
+`POST /projects`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/projects \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Creates a new project within the organization specified in the JWT token.
+Organization ID is automatically extracted from the token and does not need to be provided.
+
+> Payload
+
+```json
+{
+ "id": "default-project",
+ "displayName": "Default Project",
+ "description": "This is the default project for development"
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|body|body|[CreateProjectRequest](schemas.md#schemacreateprojectrequest)|true|none|
+
+> Example responses
+>
+> 201 Response
+
+```json
+{
+ "id": "default-project",
+ "displayName": "Default Project",
+ "description": "This is the default project for development",
+ "organizationId": "acme",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Project created successfully|[Project](schemas.md#schemaproject)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Get all projects for current user's organization
+
+
+
+`GET /projects`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/projects \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves all projects belonging to the organization specified in the JWT token.
+Organization ID is automatically extracted from the token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+|sortBy|query|string|false|Field to sort the collection by. An unrecognized value falls back to the default sort (createdAt).|
+|sortOrder|query|string|false|Sort direction applied to `sortBy`.|
+|query|query|string|false|Case-insensitive substring filter matched against the resource id (handle).|
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|sortBy|name|
+|sortBy|createdAt|
+|sortOrder|asc|
+|sortOrder|desc|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "default-project",
+ "displayName": "Default Project",
+ "description": "This is the default project for development",
+ "organizationId": "acme",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Projects retrieved successfully|[ProjectListResponse](schemas.md#schemaprojectlistresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get project by ID
+
+
+
+`GET /projects/{projectId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/projects/{projectId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves a specific project by its ID (handle). Access is validated against the organization
+in the JWT token to ensure users can only access projects in their organization.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|projectId|path|string|true|**Project ID** consisting of the **handle** (unique slug identifier) of the Project.|
+
+#### Detailed descriptions
+
+**projectId**: **Project ID** consisting of the **handle** (unique slug identifier) of the Project.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "id": "default-project",
+ "displayName": "Default Project",
+ "description": "This is the default project for development",
+ "organizationId": "acme",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Project retrieved successfully|[Project](schemas.md#schemaproject)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Update project
+
+
+
+`PUT /projects/{projectId}`
+
+> Code samples
+
+```shell
+
+curl -X PUT https://localhost:9243/api/v0.9/projects/{projectId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Updates an existing project's name. Access is validated against the organization
+in the JWT token.
+
+> Payload
+
+```json
+{
+ "displayName": "Default Project",
+ "description": "This is the default project for development"
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|projectId|path|string|true|**Project ID** consisting of the **handle** (unique slug identifier) of the Project.|
+|body|body|[Project](schemas.md#schemaproject)|true|none|
+
+#### Detailed descriptions
+
+**projectId**: **Project ID** consisting of the **handle** (unique slug identifier) of the Project.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "id": "default-project",
+ "displayName": "Default Project",
+ "description": "This is the default project for development",
+ "organizationId": "acme",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Project updated successfully|[Project](schemas.md#schemaproject)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete project
+
+
+
+`DELETE /projects/{projectId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/projects/{projectId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes a specific project by its handle (unique slug identifier). Access is validated
+against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|projectId|path|string|true|**Project ID** consisting of the **handle** (unique slug identifier) of the Project.|
+
+#### Detailed descriptions
+
+**projectId**: **Project ID** consisting of the **handle** (unique slug identifier) of the Project.
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Project deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/rest-api-deployments.md b/docs/rest-apis/platform-api/rest-api-deployments.md
new file mode 100644
index 0000000000..685aa8a548
--- /dev/null
+++ b/docs/rest-apis/platform-api/rest-api-deployments.md
@@ -0,0 +1,786 @@
+
REST API Deployments
+
+API deployment artifact management and lifecycle operations
+
+## Create and deploy a new deployment
+
+
+
+`POST /rest-apis/{restApiId}/deployments`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/rest-apis/{restApiId}/deployments \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Creates an immutable deployment artifact for an API and deploys it to a specified gateway.
+Each deployment targets a single gateway. The apiId parameter is the API handle (identifier),
+not the UUID. The operation returns a transitional DEPLOYING status. Final success or failure will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+Access is validated against the organization in the JWT token.
+
+> Payload
+
+```json
+{
+ "name": "v1.0-production",
+ "base": "current",
+ "gatewayId": "prod-gateway-01",
+ "metadata": {}
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|body|body|[DeployRequest](schemas.md#schemadeployrequest)|true|Deployment request with gateway ID, base reference, and metadata|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes DEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|API deployed successfully|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Get deployments for a REST API
+
+
+
+`GET /rest-apis/{restApiId}/deployments`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/rest-apis/{restApiId}/deployments \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves all deployment artifacts for a specific API. The apiId parameter is the API handle (identifier),
+not the UUID. Supports filtering by gateway handle and deployment status.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|gatewayId|query|string|false|**Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway to filter status by.|
+|status|query|string|false|Filter deployments by status (DEPLOYED, UNDEPLOYED, DEPLOYING, UNDEPLOYING, FAILED, or ARCHIVED)|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+**gatewayId**: **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway to filter status by.
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|status|DEPLOYED|
+|status|UNDEPLOYED|
+|status|DEPLOYING|
+|status|UNDEPLOYING|
+|status|FAILED|
+|status|ARCHIVED|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 0,
+ "list": [
+ {
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYED",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "statusReason": "string",
+ "updatedAt": "2019-08-24T14:15:22Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Deployments retrieved successfully|[DeploymentListResponse](schemas.md#schemadeploymentlistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get deployment by ID
+
+
+
+`GET /rest-apis/{restApiId}/deployments/{deploymentId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/rest-apis/{restApiId}/deployments/{deploymentId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves metadata for a specific deployment artifact including status, gateway association,
+and timestamps. Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|deploymentId|path|string(uuid)|true|The UUID of the deployment|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYED",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "statusReason": "string",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Deployment metadata retrieved successfully|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete deployment
+
+
+
+`DELETE /rest-apis/{restApiId}/deployments/{deploymentId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/rest-apis/{restApiId}/deployments/{deploymentId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes a deployment artifact. Deletion is only allowed when the deployment is in UNDEPLOYED status.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|deploymentId|path|string(uuid)|true|The UUID of the deployment|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "DEPLOYMENT_ACTIVE",
+ "message": "Cannot delete an active deployment - undeploy it first."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Deployment deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The deployment is still active and must be undeployed before deletion.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Undeploy deployment from gateway
+
+
+
+`POST /rest-apis/{restApiId}/deployments/{deploymentId}/undeploy`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/rest-apis/{restApiId}/deployments/{deploymentId}/undeploy?gatewayId=string \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Undeploys an active deployment, stopping the API from being served on the specified gateway.
+The deployment artifact remains in the system and can be restored later.
+Returns the updated deployment object with initial status UNDEPLOYING. Final status (UNDEPLOYED or FAILED) will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+
+The gatewayId query parameter is validated against deployment's bound gateway to prevent unintended operations.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|deploymentId|path|string|true|UUID of the deployment to undeploy|
+|gatewayId|query|string|true|Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes UNDEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "UNDEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Undeploy initiated successfully. Returns the deployment with initial status UNDEPLOYING. Poll status for final result.|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Restore a previous deployment
+
+
+
+`POST /rest-apis/{restApiId}/deployments/{deploymentId}/restore`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/rest-apis/{restApiId}/deployments/{deploymentId}/restore?gatewayId=string \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Initiates restoring a previous deployment (ARCHIVED or UNDEPLOYED) on the specified gateway.
+Returns the deployment with initial status DEPLOYING. Final success or failure will be reported asynchronously via the deployment's status and statusReason once the gateway acknowledges.
+The target deployment must not already be in DEPLOYED status.
+
+The gatewayId query parameter is validated against the deployment's bound gateway to prevent unintended operations.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|deploymentId|path|string|true|UUID of the deployment to restore (must be ARCHIVED or UNDEPLOYED)|
+|gatewayId|query|string|true|Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> Asynchronous operation accepted; poll the deployment until status becomes DEPLOYED or FAILED.
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYING",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Restore initiated successfully. Returns the deployment with initial status DEPLOYING. Poll status for final result.|[DeploymentResponse](schemas.md#schemadeploymentresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/rest-apis.md b/docs/rest-apis/platform-api/rest-apis.md
new file mode 100644
index 0000000000..9a69c34382
--- /dev/null
+++ b/docs/rest-apis/platform-api/rest-apis.md
@@ -0,0 +1,1900 @@
+
REST APIs
+
+API management operations
+
+## Get all REST APIs for an organization
+
+
+
+`GET /rest-apis`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/rest-apis?projectId=default-project \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves all REST APIs belonging to an organization.
+Requires the projectId query parameter to filter APIs by project.
+Provide name and version together to check uniqueness (empty result = name/version combination is available).
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|API created successfully|[RESTAPI](schemas.md#schemarestapi)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Get REST API by ID
+
+
+
+`GET /rest-apis/{restApiId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/rest-apis/{restApiId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves a specific API by its ID (handle). Access is validated against the organization
+in the JWT token.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|API updated successfully|[RESTAPI](schemas.md#schemarestapi)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete REST API
+
+
+
+`DELETE /rest-apis/{restApiId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/rest-apis/{restApiId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Deletes a specific API by its ID (handle). Access is validated against the organization
+in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|API deleted successfully|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get gateways for REST API
+
+
+
+`GET /rest-apis/{restApiId}/gateways`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/rest-apis/{restApiId}/gateways \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Retrieves all gateways associated with the specified API, including deployment details.
+Returns gateway information along with association timestamps and deployment status.
+Access is validated against the organization in the JWT token.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 3,
+ "list": [
+ {
+ "id": "prod-gateway-01",
+ "organizationId": "acme",
+ "displayName": "Production Gateway 01",
+ "description": "Production gateway for handling API traffic",
+ "properties": {
+ "region": "us-west",
+ "tier": "premium"
+ },
+ "endpoints": [
+ "https://api.example.com:8443/api/v1",
+ "wss://events.example.com:8444"
+ ],
+ "isCritical": true,
+ "functionalityType": "regular",
+ "version": "1.0",
+ "isActive": true,
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2025-10-14T10:30:00Z",
+ "updatedAt": "2025-10-14T10:30:00Z",
+ "associatedAt": "2025-10-15T10:30:00Z",
+ "isDeployed": true,
+ "deployment": {
+ "status": "CREATED",
+ "deployedAt": "2025-10-15T11:00:00Z"
+ }
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of gateways associated with the API, including deployment details|[RESTAPIGatewayListResponse](schemas.md#schemarestapigatewaylistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Add gateways for REST API
+
+
+
+`POST /rest-apis/{restApiId}/gateways`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/rest-apis/{restApiId}/gateways \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Associates gateways to the specified API. If gateways are already associated,
+updates the association timestamp. Returns all gateways associated with the API
+including deployment details. Access is validated against the organization
+in the JWT token.
+
+> Payload
+
+```json
+[
+ {
+ "gatewayId": "prod-gateway-01"
+ }
+]
+```
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of all gateways associated with the API, including deployment details|[RESTAPIGatewayListResponse](schemas.md#schemarestapigatewaylistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Create API key
+
+
+
+`POST /rest-apis/{restApiId}/api-keys`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/rest-apis/{restApiId}/api-keys \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Creates a new API key for the specified API. The API key will be hashed before
+storage and broadcasted to all gateways where the API is deployed. This endpoint
+allows external platforms to inject API keys to hybrid gateways.
+
+> Payload
+
+```json
+{
+ "id": "production-key-01",
+ "displayName": "Production API Key",
+ "apiKey": "sk_example_1234567890abcdef",
+ "externalRefId": "ext-ref-12345",
+ "expiresAt": "2026-12-31T23:59:59Z",
+ "expiresIn": {
+ "duration": 30,
+ "unit": "days"
+ },
+ "issuer": "api-platform-devportal"
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|body|body|[CreateAPIKeyRequest](schemas.md#schemacreateapikeyrequest)|true|API key creation request|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> 201 Response
+
+```json
+{
+ "status": "success",
+ "message": "API key created and broadcasted to gateways successfully",
+ "keyId": "production-key-01"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+> 503 Response
+
+```json
+{
+ "status": "error",
+ "code": "GATEWAY_CONNECTION_UNAVAILABLE",
+ "message": "No gateway connections are currently available.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|API key created successfully|[CreateAPIKeyResponse](schemas.md#schemacreateapikeyresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. No gateway connections are currently available to service this request.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## Update API key
+
+
+
+`PUT /rest-apis/{restApiId}/api-keys/{apiKeyId}`
+
+> Code samples
+
+```shell
+
+curl -X PUT https://localhost:9243/api/v0.9/rest-apis/{restApiId}/api-keys/{apiKeyId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Updates an existing API key for the specified API. The new API key value will
+be hashed before storage and broadcasted to all gateways where the API is deployed.
+This endpoint allows external platforms to rotate API keys on hybrid gateways.
+
+> Payload
+
+```json
+{
+ "name": "production-key-01",
+ "displayName": "Production API Key (Updated)",
+ "apiKey": "sk_example_new1234567890abcdef",
+ "externalRefId": "ext-ref-12345",
+ "expiresAt": "2027-12-31T23:59:59Z",
+ "expiresIn": {
+ "duration": 30,
+ "unit": "days"
+ },
+ "issuer": "api-platform-devportal"
+}
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|apiKeyId|path|string|true|The unique name/identifier of the API key|
+|body|body|[UpdateAPIKeyRequest](schemas.md#schemaupdateapikeyrequest)|true|API key update request|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "status": "success",
+ "message": "API key updated and broadcasted to gateways successfully",
+ "keyId": "production-key-01"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+> 503 Response
+
+```json
+{
+ "status": "error",
+ "code": "GATEWAY_CONNECTION_UNAVAILABLE",
+ "message": "No gateway connections are currently available.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|API key updated successfully|[UpdateAPIKeyResponse](schemas.md#schemaupdateapikeyresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. No gateway connections are currently available to service this request.|[Error](schemas.md#schemaerror)|
+
+## Revoke API key
+
+
+
+`DELETE /rest-apis/{restApiId}/api-keys/{apiKeyId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/rest-apis/{restApiId}/api-keys/{apiKeyId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Revokes an API key for the specified API. The revocation will be broadcasted
+to all gateways where the API is deployed. This endpoint allows external platforms
+to revoke API keys on hybrid gateways.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|restApiId|path|string|true|**API ID** consisting of the **handle** (unique identifier) of the API.|
+|apiKeyId|path|string|true|The unique name/identifier of the API key to revoke|
+
+#### Detailed descriptions
+
+**restApiId**: **API ID** consisting of the **handle** (unique identifier) of the API.
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+> 503 Response
+
+```json
+{
+ "status": "error",
+ "code": "GATEWAY_CONNECTION_UNAVAILABLE",
+ "message": "No gateway connections are currently available.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|API key revoked successfully (no content)|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. No gateway connections are currently available to service this request.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/schemas.md b/docs/rest-apis/platform-api/schemas.md
new file mode 100644
index 0000000000..e6b8c72b32
--- /dev/null
+++ b/docs/rest-apis/platform-api/schemas.md
@@ -0,0 +1,5424 @@
+# Schemas
+
+
APIKeyItem
+
+
+
+
+
+
+```json
+{
+ "id": "string",
+ "displayName": "string",
+ "maskedApiKey": "string",
+ "status": "active",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "createdBy": "john.doe",
+ "updatedAt": "2019-08-24T14:15:22Z",
+ "expiresAt": "2019-08-24T14:15:22Z",
+ "issuer": "api-platform-devportal",
+ "allowedTargets": "string"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|URL-safe handle (identifier) of the API key (generated from displayName when not supplied)|
+|displayName|string|true|none|Human-readable display name of the API key|
+|maskedApiKey|string|true|none|Masked representation of the API key for display purposes|
+|status|string|true|none|Current status of the key|
+|createdAt|string(date-time)|true|none|Timestamp when the key was created|
+|createdBy|string|true|read-only|User identifier of the user who created this resource|
+|updatedAt|string(date-time)|true|none|Timestamp when the key was last updated|
+|expiresAt|string(date-time)|false|none|Optional expiration timestamp|
+|issuer|string|false|none|Optional identifier of the developer portal that provisioned this key|
+|allowedTargets|string|true|none|Comma-separated list of allowed gateways; 'ALL' means unrestricted|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|active|
+|status|revoked|
+|status|expired|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "acme",
+ "displayName": "Acme Corporation",
+ "region": "us",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|true|read-only|Handle (URL-friendly slug) for the organization|
+|displayName|string|true|none|Human-readable name for the organization|
+|region|string|true|none|Geographic region where the organization operates|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource|
+|createdAt|string(date-time)|false|read-only|Timestamp when the organization was created|
+|updatedAt|string(date-time)|false|read-only|Timestamp when the organization was last updated|
+
+
CreateProjectRequest
+
+
+
+
+
+
+```json
+{
+ "id": "default-project",
+ "displayName": "Default Project",
+ "description": "This is the default project for development"
+}
+
+```
+
+Request body for creating a project. Organization ID is automatically extracted
+from the JWT token and does not need to be provided.
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Handle (URL-friendly slug) for the project. Auto-generated from displayName if not provided.|
+|displayName|string|true|none|Human-readable name for the project|
+|description|string|false|none|Description of the project|
+
+
Project
+
+
+
+
+
+
+```json
+{
+ "id": "default-project",
+ "displayName": "Default Project",
+ "description": "This is the default project for development",
+ "organizationId": "acme",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|true|read-only|Handle (URL-friendly slug) for the project|
+|displayName|string|true|none|Human-readable name for the project|
+|description|string|false|none|Description of the project|
+|organizationId|string|true|read-only|Handle (URL-friendly slug) of the organization this project belongs to|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource. Only present in the detail response (GET /projects/{projectId}), omitted from list responses.|
+|createdAt|string(date-time)|false|read-only|Timestamp when the project was created|
+|updatedAt|string(date-time)|false|read-only|Timestamp when the project was last updated|
+
+
ApplicationType
+
+
+
+
+
+
+```json
+"genai"
+
+```
+
+Type of the application
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|string|false|none|Type of the application|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|*anonymous*|genai|
+
+
CreateApplicationRequest
+
+
+
+
+
+
+```json
+{
+ "id": "my-app-handle",
+ "displayName": "GenAI Demo App",
+ "projectId": "default-project",
+ "type": "genai",
+ "description": "Sample GenAI application"
+}
+
+```
+
+Request body for creating an application.
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Unique handle/identifier for the application. Can be provided during creation or auto-generated.|
+|displayName|string|true|none|Human-readable name for the application|
+|projectId|string|true|none|Handle (URL-friendly slug) of the project this application belongs to.|
+|type|[ApplicationType](#schemaapplicationtype)|true|none|Type of the application|
+|description|string|false|none|Description of the application|
+
+
Application
+
+
+
+
+
+
+```json
+{
+ "id": "my-app-handle",
+ "displayName": "GenAI Demo App",
+ "projectId": "default-project",
+ "type": "genai",
+ "description": "Sample GenAI application",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|true|none|Application handle/identifier|
+|displayName|string|true|none|Human-readable name for the application|
+|projectId|string|true|none|Handle (URL-friendly slug) of the project this application belongs to|
+|type|[ApplicationType](#schemaapplicationtype)|true|none|Type of the application|
+|description|string|false|none|Description of the application|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource. Only present in the detail response (GET /applications/{appId}), omitted from list responses.|
+|createdAt|string(date-time)|false|read-only|none|
+|updatedAt|string(date-time)|false|read-only|none|
+
+
+
+
+
+
+
+
+```json
+{
+ "apiKeys": [
+ {
+ "keyId": "client-key-1",
+ "associatedEntity": {
+ "id": "pizza-api"
+ }
+ }
+ ]
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|apiKeys|[[APIKeyMappingSelector](#schemaapikeymappingselector)]|true|none|List of API key selectors to add to the application mappings|
+
+
AddApplicationAssociationsRequest
+
+
+
+
+
+
+```json
+{
+ "associations": [
+ {
+ "id": "provider-handle",
+ "kind": "LlmProvider"
+ }
+ ]
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|associations|[[ApplicationAssociationSelector](#schemaapplicationassociationselector)]|true|none|List of association selectors to add to the application|
+
+
ApplicationAssociationSelector
+
+
+
+
+
+
+```json
+{
+ "id": "provider-handle",
+ "kind": "LlmProvider"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|true|none|ID or handle of the association target|
+|kind|string|true|none|Type of the association target|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|kind|LlmProvider|
+|kind|LlmProxy|
+
+
ApplicationAssociation
+
+
+
+
+
+
+```json
+{
+ "id": "provider-handle",
+ "displayName": "OpenAI Provider",
+ "version": "v1.0",
+ "kind": "LlmProvider",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|true|none|Handle/ID of the associated target|
+|displayName|string|true|none|Human-readable name for the associated target|
+|version|string|true|none|Version of the associated target|
+|kind|string|true|none|Type of associated target|
+|createdAt|string(date-time)|false|none|Timestamp when the association was created|
+|updatedAt|string(date-time)|false|none|Timestamp when the association was updated|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "pizza-api"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|true|none|ID of the entity that owns the API key|
+
+
AssociatedEntity
+
+
+
+
+
+
+```json
+{
+ "id": "pizza-api",
+ "kind": "RestApi"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|true|none|ID of the associated entity|
+|kind|string|true|none|Type of the associated entity|
+
+
MappedAPIKey
+
+
+
+
+
+
+```json
+{
+ "keyId": "client-key-1",
+ "associatedEntity": {
+ "id": "pizza-api",
+ "kind": "RestApi"
+ },
+ "status": "ACTIVE",
+ "userId": "john.doe",
+ "createdAt": "2025-11-15T10:30:00Z",
+ "updatedAt": "2025-11-15T11:30:00Z",
+ "expiresAt": "2026-11-15T10:30:00Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|keyId|string|true|none|Name of the API key|
+|associatedEntity|[AssociatedEntity](#schemaassociatedentity)|true|none|none|
+|status|string|false|none|Status of the API key|
+|userId|string|false|read-only|User identifier of the user who created this resource|
+|createdAt|string(date-time)|false|none|Timestamp when the API key was created|
+|updatedAt|string(date-time)|false|none|Timestamp when the API key was updated|
+|expiresAt|string(date-time)|false|none|Expiration timestamp of the API key|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "my-rest-api-handle",
+ "displayName": "PizzaShackAPI",
+ "description": "This is a simple API for Pizza Shack online pizza delivery store",
+ "context": "/pizza",
+ "version": "1.0.0",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "projectId": "default-project",
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z",
+ "readOnly": false,
+ "upstream": {
+ "main": {
+ "url": "http://prod-backend:5000/api/v2",
+ "auth": {
+ "type": "api-key",
+ "header": "X-API-Key",
+ "value": "my-api-key-value"
+ }
+ },
+ "sandbox": {
+ "url": "http://prod-backend:5000/api/v2",
+ "auth": {
+ "type": "api-key",
+ "header": "X-API-Key",
+ "value": "my-api-key-value"
+ }
+ }
+ },
+ "lifeCycleStatus": "CREATED",
+ "kind": "RestApi",
+ "transport": [
+ "http",
+ "https"
+ ],
+ "policies": [
+ {
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+ }
+ ],
+ "operations": [
+ {
+ "name": "getPetById",
+ "description": "Find pet by ID",
+ "request": {
+ "method": "GET",
+ "path": "/pet/{petId}",
+ "policies": [
+ {
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+ }
+ ]
+ }
+ }
+ ],
+ "channels": [
+ {
+ "name": "issues",
+ "description": "Channel for order events",
+ "request": {
+ "method": "SUB",
+ "name": "issues",
+ "policies": [
+ {
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+ }
+ ]
+ }
+ }
+ ],
+ "subscriptionPlans": [
+ "Gold",
+ "Silver"
+ ]
+}
+
+```
+
+API object
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Unique handle/identifier for the API. Can be provided during creation or auto-generated. On update (PUT), if provided must match the path parameter — returns 400 if they differ.|
+|displayName|string|true|none|Human-readable name for the API|
+|description|string|false|none|none|
+|context|string|true|none|none|
+|version|string|true|none|none|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource. Only present in the detail response (GET /rest-apis/{apiId}), omitted from list responses.|
+|projectId|string|true|none|Handle (URL-friendly slug) of the project this API belongs to|
+|createdAt|string(date-time)|false|read-only|Timestamp when the api was created|
+|updatedAt|string(date-time)|false|read-only|Timestamp when the api was last updated|
+|readOnly|boolean|false|read-only|True if the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane; false for control-plane created artifacts.|
+|upstream|[Upstream](#schemaupstream)|true|none|Upstream backend configuration with main and sandbox endpoints|
+|lifeCycleStatus|string|false|none|Current lifecycle status of the API|
+|kind|string|false|none|Kind of the API based on its communication protocol or architectural style|
+|transport|[string]|false|none|Supported transports for the API (http and/or https)|
+|policies|[[Policy](#schemapolicy)]|false|none|List of policies to be applied on the API|
+|operations|[[Operation](#schemaoperation)]|false|none|List of operations exposed by this API|
+|channels|[[Channel](#schemachannel)]|false|none|List of channels exposed by this API|
+|subscriptionPlans|[string]|false|none|List of subscription plan names enabled for this API (e.g. Gold, Silver). When set, only these plans can be used when subscribing to the API.|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|lifeCycleStatus|STAGED|
+|lifeCycleStatus|CREATED|
+|lifeCycleStatus|PUBLISHED|
+|lifeCycleStatus|DEPRECATED|
+|lifeCycleStatus|RETIRED|
+|lifeCycleStatus|BLOCKED|
+
+
+
+
+
+
+
+
+```json
+{
+ "enabled": true,
+ "key": "X-API-Key",
+ "valuePrefix": "Bearer",
+ "in": "header"
+}
+
+```
+
+API Key Security
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|enabled|boolean|false|none|Whether API key authentication is enabled|
+|key|string|false|none|Name of the header or query parameter to be used for the API key|
+|valuePrefix|string|false|none|Optional prefix to strip from the inbound API key value before validation, for example "Bearer"|
+|in|string|false|none|Location of the API key (header or query)|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|in|header|
+|in|query|
+
+
Operation
+
+
+
+
+
+
+```json
+{
+ "name": "getPetById",
+ "description": "Find pet by ID",
+ "request": {
+ "method": "GET",
+ "path": "/pet/{petId}",
+ "policies": [
+ {
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+ }
+ ]
+ }
+}
+
+```
+
+API Operation
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|name|string|false|none|Name of the operation|
+|description|string|false|none|Description of the operation|
+|request|[OperationRequest](#schemaoperationrequest)|true|none|Request details for an API operation|
+
+
Channel
+
+
+
+
+
+
+```json
+{
+ "name": "issues",
+ "description": "Channel for order events",
+ "request": {
+ "method": "SUB",
+ "name": "issues",
+ "policies": [
+ {
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+ }
+ ]
+ }
+}
+
+```
+
+Async API Channel
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|name|string|false|none|Name of the channel|
+|description|string|false|none|Description of the channel|
+|request|[ChannelRequest](#schemachannelrequest)|true|none|Request details for a channel within the Async API|
+
+
+
+
+
+
+
+
+```json
+{
+ "method": "SUB",
+ "name": "issues",
+ "policies": [
+ {
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+ }
+ ]
+}
+
+```
+
+Channel Request
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|method|string|true|none|Async method for the channel|
+|name|string|true|none|Channel name for the event|
+|policies|[[Policy](#schemapolicy)]|false|none|List of policies to be applied on the operation|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|method|SUB|
+
+
Policy
+
+
+
+
+
+
+```json
+{
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+}
+
+```
+
+Policy Configuration
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|executionCondition|string|false|none|Conditional expression that determines when this policy executes|
+|name|string|true|none|Name of the policy to apply|
+|params|object|false|none|Key-value pairs of parameters for the policy|
+|version|string|true|none|Version of the policy. Only major-only version is allowed (e.g., v0, v1). Full semantic version (e.g., v1.0.0) is not accepted and will be rejected.|
+
+
AddGatewayToRESTAPIRequest
+
+
+
+
+
+
+```json
+{
+ "gatewayId": "prod-gateway-01"
+}
+
+```
+
+AddGatewayToAPIRequest object with basic gateway details
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|gatewayId|string|true|none|Handle (URL-friendly slug) of the gateway to associate with the REST API|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "prod-gateway-01",
+ "organizationId": "acme",
+ "displayName": "Production Gateway 01",
+ "description": "Production gateway for handling API traffic",
+ "properties": {
+ "region": "us-west",
+ "tier": "premium"
+ },
+ "endpoints": [
+ "https://api.example.com:8443/api/v1",
+ "wss://events.example.com:8444"
+ ],
+ "isCritical": true,
+ "functionalityType": "regular",
+ "version": "1.0",
+ "isActive": true,
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2025-10-14T10:30:00Z",
+ "updatedAt": "2025-10-14T10:30:00Z",
+ "associatedAt": "2025-10-15T10:30:00Z",
+ "isDeployed": true,
+ "deployment": {
+ "status": "CREATED",
+ "deployedAt": "2025-10-15T11:00:00Z"
+ }
+}
+
+```
+
+### Properties
+
+allOf
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|[GatewayResponse](#schemagatewayresponse)|false|none|none|
+
+and
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|object|false|none|none|
+|» associatedAt|string(date-time)|true|none|Timestamp when the gateway was associated with the API|
+|» isDeployed|boolean|true|none|Whether the API is currently deployed to this gateway|
+|» deployment|[RESTAPIDeploymentDetails](#schemarestapideploymentdetails)|false|none|Deployment details (only present when isDeployed is true)|
+
+
RESTAPIDeploymentDetails
+
+
+
+
+
+
+```json
+{
+ "status": "CREATED",
+ "deployedAt": "2025-10-15T11:00:00Z"
+}
+
+```
+
+Details about API deployment to a specific gateway
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|status|string|true|none|Current deployment status|
+|deployedAt|string(date-time)|true|none|Timestamp when the API was deployed|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|CREATED|
+|status|APPROVED|
+|status|REJECTED|
+
+
CreateGatewayRequest
+
+
+
+
+
+
+```json
+{
+ "id": "prod-gateway-01",
+ "displayName": "Production Gateway 01",
+ "description": "Production gateway for handling API traffic",
+ "endpoints": [
+ "https://api.example.com:8443/api/v1",
+ "wss://events.example.com:8444"
+ ],
+ "isCritical": true,
+ "functionalityType": "regular",
+ "properties": {
+ "region": "us-west",
+ "tier": "premium"
+ },
+ "version": "1.0"
+}
+
+```
+
+Request body for creating a gateway. Organization ID is automatically extracted
+from the JWT token and does not need to be provided.
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Handle (URL-friendly slug) for the gateway. Immutable after creation.|
+|displayName|string|true|none|Human-readable gateway name|
+|description|string|false|none|Description of the gateway|
+|endpoints|[string]|true|none|Network endpoints exposed by this gateway, each as a full URL string|
+|isCritical|boolean|false|none|Whether the gateway is critical for production|
+|functionalityType|string|true|none|Type of gateway functionality|
+|properties|object|false|none|Custom key-value properties for the gateway|
+|version|string|false|none|Gateway version in `major.minor` format (e.g. `1.0`) or CalVer `YYYY.MM.DD` format (e.g. `2026.05.13`). Defaults to `1.0` if not provided.|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|functionalityType|regular|
+|functionalityType|ai|
+|functionalityType|event|
+
+
CustomPolicyResponse
+
+
+
+
+
+
+```json
+{
+ "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "organizationUuid": "bc554ded-7e40-44a7-b397-48480793ad03",
+ "name": "rate-limit-custom",
+ "version": "1.0.0",
+ "description": "Custom rate limiting policy",
+ "policyDefinition": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+
+```
+
+A custom policy stored in the platform's custom policy registry.
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|uuid|string(uuid)|true|none|Unique identifier of the custom policy record|
+|organizationUuid|string(uuid)|true|none|Organization this policy belongs to|
+|name|string|true|none|Policy name|
+|version|string|true|none|Policy version|
+|description|string|false|none|Human-readable description of the policy|
+|policyDefinition|object|true|none|The full policy schema as declared in the policy's policy-definition.yaml. Contains `parameters` and `systemParameters` JSON Schema documents.|
+|createdAt|string(date-time)|false|none|none|
+|updatedAt|string(date-time)|false|none|none|
+
+
GatewayPolicyDefinition
+
+
+
+
+
+
+```json
+{
+ "name": "set-wso2-headers",
+ "version": "v0.8.0",
+ "description": "Sets WSO2-specific headers in the request and response.",
+ "isCustomPolicy": true,
+ "policyDefinition": {}
+}
+
+```
+
+A policy installed on a gateway controller.
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|name|string|true|none|Unique policy name|
+|version|string|true|none|Semantic version of the policy|
+|description|string|false|none|Human-readable description of the policy|
+|isCustomPolicy|boolean|true|none|Whether this is a user-installed custom policy.|
+|policyDefinition|object|false|none|The full policy schema as declared in the policy's policy-definition.yaml. Contains `parameters` and `systemParameters` JSON Schema documents. Only present for custom policies.|
+
+
ManifestSyncResponse
+
+
+
+
+
+
+```json
+{
+ "policies": [
+ {
+ "name": "set-wso2-headers",
+ "version": "v0.8.0",
+ "description": "Sets WSO2-specific headers in the request and response.",
+ "isCustomPolicy": true,
+ "policyDefinition": {}
+ }
+ ]
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|policies|[[GatewayPolicyDefinition](#schemagatewaypolicydefinition)]|false|none|All policies installed on the gateway. Each entry includes name, version, and isCustomPolicy. Custom policies additionally include policyDefinition with their parameters and systemParameters schemas.|
+
+
GatewayResponse
+
+
+
+
+
+
+```json
+{
+ "id": "prod-gateway-01",
+ "organizationId": "acme",
+ "displayName": "Production Gateway 01",
+ "description": "Production gateway for handling API traffic",
+ "properties": {
+ "region": "us-west",
+ "tier": "premium"
+ },
+ "endpoints": [
+ "https://api.example.com:8443/api/v1",
+ "wss://events.example.com:8444"
+ ],
+ "isCritical": true,
+ "functionalityType": "regular",
+ "version": "1.0",
+ "isActive": true,
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2025-10-14T10:30:00Z",
+ "updatedAt": "2025-10-14T10:30:00Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|read-only|Handle (URL-friendly slug) for the gateway|
+|organizationId|string|false|none|Handle (URL-friendly slug) of the organization this gateway belongs to|
+|displayName|string|true|none|Human-readable gateway name|
+|description|string|false|none|Description of the gateway|
+|properties|object|false|none|Custom key-value properties for the gateway|
+|endpoints|[string]|false|none|Network endpoints exposed by this gateway, each as a full URL string|
+|isCritical|boolean|false|none|Whether the gateway is critical for production|
+|functionalityType|string|false|none|Type of gateway functionality|
+|version|string|false|none|Gateway version in `major.minor` format (e.g. `1.0`) or CalVer `YYYY.MM.DD` format (e.g. `2026.05.13`)|
+|isActive|boolean|false|none|Indicates if the gateway is currently connected to the platform via WebSocket|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource. Only present in the detail response (GET /gateways/{gatewayId}), omitted from list responses.|
+|createdAt|string(date-time)|false|none|Timestamp when gateway was registered|
+|updatedAt|string(date-time)|false|none|Timestamp when gateway was last updated|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|functionalityType|regular|
+|functionalityType|ai|
+|functionalityType|event|
+
+
Pagination
+
+
+
+
+
+
+```json
+{
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|total|integer|true|none|Total number of items available across all pages|
+|offset|integer|true|none|Zero-based index of first item in current response|
+|limit|integer|true|none|Maximum number of items returned per page|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "prod-gateway-01",
+ "isActive": true,
+ "isCritical": true
+}
+
+```
+
+Lightweight gateway status information optimized for frequent polling
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|read-only|Handle (URL-friendly slug) for the gateway|
+|isActive|boolean|false|none|Indicates if the gateway is currently connected to the platform via WebSocket|
+|isCritical|boolean|false|none|Whether the gateway is critical for production|
+
+
GatewayStatusListResponse
+
+
+
+
+
+
+```json
+{
+ "count": 2,
+ "list": [
+ {
+ "id": "prod-gateway-01",
+ "isActive": true,
+ "isCritical": true
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+
+```
+
+List of gateway status information for polling
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|count|integer|true|none|Number of items in current response|
+|list|[[GatewayStatusResponse](#schemagatewaystatusresponse)]|true|none|[Lightweight gateway status information optimized for frequent polling]|
+|pagination|[Pagination](#schemapagination)|true|none|none|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "def45678-g901-23hi-j456-789012klmnop",
+ "token": "REDACTED_TOKEN",
+ "createdAt": "2025-10-15T14:20:00Z",
+ "message": "New token generated successfully. Old token remains active until revoked."
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string(uuid)|false|none|ID of the newly generated token|
+|token|string|false|none|Plain-text new authentication token (only exposed once during rotation). The example value is a non-functional placeholder.|
+|createdAt|string(date-time)|false|none|Timestamp when new token was created|
+|message|string|false|none|Informational message about token rotation|
+
+
+
+
+
+
+
+
+```json
+"days"
+
+```
+
+Time unit for API key expiration duration
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|string|false|none|Time unit for API key expiration duration|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|*anonymous*|seconds|
+|*anonymous*|minutes|
+|*anonymous*|hours|
+|*anonymous*|days|
+|*anonymous*|weeks|
+|*anonymous*|months|
+
+
ExpirationDuration
+
+
+
+
+
+
+```json
+{
+ "duration": 30,
+ "unit": "days"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|duration|integer|true|none|Duration value (must be positive)|
+|unit|[TimeUnit](#schematimeunit)|true|none|Time unit for API key expiration duration|
+
+
CreateAPIKeyRequest
+
+
+
+
+
+
+```json
+{
+ "id": "production-key-01",
+ "displayName": "Production API Key",
+ "apiKey": "sk_example_1234567890abcdef",
+ "externalRefId": "ext-ref-12345",
+ "expiresAt": "2026-12-31T23:59:59Z",
+ "expiresIn": {
+ "duration": 30,
+ "unit": "days"
+ },
+ "issuer": "api-platform-devportal"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Unique identifier for this API key within the API (optional; if omitted, generated from displayName)|
+|displayName|string|true|none|Human-readable name for the API key|
+|apiKey|string|true|none|The plain text API key value that will be hashed before storage|
+|externalRefId|string¦null|false|none|Optional reference ID for tracing purposes (from external platforms)|
+|expiresAt|string(date-time)¦null|false|none|Optional expiration time in ISO 8601 format|
+|expiresIn|[ExpirationDuration](#schemaexpirationduration)|false|none|Optional expiration duration|
+|issuer|string¦null|false|none|Identifier of the developer portal that provisioned this API key. Null if not provided.|
+
+
CreateAPIKeyResponse
+
+
+
+
+
+
+```json
+{
+ "status": "success",
+ "message": "API key created and broadcasted to gateways successfully",
+ "keyId": "production-key-01"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|status|string|true|none|Status of the operation|
+|message|string|true|none|Additional details about the operation result|
+|keyId|string|false|none|The internal ID generated for tracking|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|success|
+|status|error|
+
+
UpdateAPIKeyRequest
+
+
+
+
+
+
+```json
+{
+ "name": "production-key-01",
+ "displayName": "Production API Key (Updated)",
+ "apiKey": "sk_example_new1234567890abcdef",
+ "externalRefId": "ext-ref-12345",
+ "expiresAt": "2027-12-31T23:59:59Z",
+ "expiresIn": {
+ "duration": 30,
+ "unit": "days"
+ },
+ "issuer": "api-platform-devportal"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|name|string|false|none|Unique identifier for this API key within the API (optional; if omitted, generated from displayName)|
+|displayName|string|true|none|Human-readable name for the API key|
+|apiKey|string|true|none|The new plain text API key value that will be hashed before storage|
+|externalRefId|string¦null|false|none|Optional reference ID for tracing purposes (from external platforms)|
+|expiresAt|string(date-time)¦null|false|none|Optional expiration time in ISO 8601 format|
+|expiresIn|[ExpirationDuration](#schemaexpirationduration)|false|none|Optional expiration duration|
+|issuer|string|false|none|Identifies the portal that created this key|
+
+
UpdateAPIKeyResponse
+
+
+
+
+
+
+```json
+{
+ "status": "success",
+ "message": "API key updated and broadcasted to gateways successfully",
+ "keyId": "production-key-01"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|status|string|true|none|Status of the operation|
+|message|string|true|none|Additional details about the operation result|
+|keyId|string|false|none|The internal ID of the updated key|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|success|
+|status|error|
+
+
SubscriptionPlanLimit
+
+
+
+
+
+
+```json
+{
+ "limitType": "REQUEST_COUNT",
+ "timeUnit": "HOUR",
+ "timeAmount": 1,
+ "limitCount": 10000,
+ "limitCountUnit": "string",
+ "stopOnQuotaReach": true
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|limitType|string|false|none|Kind of quota this limit enforces. Only REQUEST_COUNT is currently enforced; other values are accepted by the schema for forward compatibility but are rejected by the API today.|
+|timeUnit|string|true|none|Throttle window unit|
+|timeAmount|integer|false|none|Number of timeUnit windows the limit applies over (e.g. 2 with timeUnit=HOUR means "per 2 hours")|
+|limitCount|integer|true|none|Number of requests (or units, for BANDWIDTH/TOTAL_TOKEN_COUNT) allowed in the throttle window|
+|limitCountUnit|string|false|none|Unit for limitCount when limitType is BANDWIDTH (e.g. MB, GB)|
+|stopOnQuotaReach|boolean|false|none|Whether to block requests when this limit's quota is exhausted|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|limitType|REQUEST_COUNT|
+|limitType|BANDWIDTH|
+|limitType|TOTAL_TOKEN_COUNT|
+|timeUnit|MINUTE|
+|timeUnit|HOUR|
+|timeUnit|DAY|
+|timeUnit|MONTH|
+
+
CreateSubscriptionPlanRequest
+
+
+
+
+
+
+```json
+{
+ "id": "gold",
+ "displayName": "Gold",
+ "limits": [
+ {
+ "limitType": "REQUEST_COUNT",
+ "timeUnit": "HOUR",
+ "timeAmount": 1,
+ "limitCount": 10000,
+ "limitCountUnit": "string",
+ "stopOnQuotaReach": true
+ }
+ ],
+ "expiryTime": "2019-08-24T14:15:22Z",
+ "status": "ACTIVE"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Handle (URL-friendly slug) for the plan. Immutable after creation.|
+|displayName|string|true|none|Human-readable name for the subscription plan|
+|limits|[[SubscriptionPlanLimit](#schemasubscriptionplanlimit)]|false|none|Throttling limits for the plan (e.g. requests per hour, requests per month). The table backing this API already supports multiple limits per plan, but the platform-api currently only persists and enforces the first entry in this array; any additional entries are accepted but ignored.|
+|expiryTime|string(date-time)|false|none|Optional plan expiry time (RFC3339)|
+|status|string|false|none|none|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|ACTIVE|
+|status|INACTIVE|
+
+
SubscriptionPlan
+
+
+
+
+
+
+```json
+{
+ "id": "string",
+ "displayName": "string",
+ "limits": [
+ {
+ "limitType": "REQUEST_COUNT",
+ "timeUnit": "HOUR",
+ "timeAmount": 1,
+ "limitCount": 10000,
+ "limitCountUnit": "string",
+ "stopOnQuotaReach": true
+ }
+ ],
+ "expiryTime": "2019-08-24T14:15:22Z",
+ "organizationId": "acme",
+ "status": "ACTIVE",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Handle (slug) for the subscription plan|
+|displayName|string|true|none|Human-readable name for the subscription plan|
+|limits|[[SubscriptionPlanLimit](#schemasubscriptionplanlimit)]|false|none|Throttling limits configured for the plan. Only one entry is currently supported and returned, even though the underlying storage allows multiple.|
+|expiryTime|string(date-time)|false|none|none|
+|organizationId|string|false|none|Handle (URL-friendly slug) of the organization this plan belongs to|
+|status|string|false|none|none|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource. Only present in the detail response (GET /subscription-plans/{planId}), omitted from list responses.|
+|createdAt|string(date-time)|false|none|none|
+|updatedAt|string(date-time)|false|none|none|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|ACTIVE|
+|status|INACTIVE|
+
+
+
+
+
+
+
+
+```json
+{
+ "artifactId": "my-rest-api",
+ "kind": "RestApi",
+ "subscriberId": "user-123",
+ "applicationId": "my-app-handle",
+ "subscriptionPlanId": "gold",
+ "status": "ACTIVE"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|artifactId|string|true|none|Handle (ID) of the artifact to subscribe to. Resolved against the table for the given kind.|
+|kind|string|true|none|Type of the artifact identified by artifactId. Determines which artifact table artifactId is resolved against.|
+|subscriberId|string|true|none|Unique subscriber identifier for the subscription (required)|
+|applicationId|string|false|none|Handle (ID) of the application this subscription belongs to. Optional in token-based subscriptions.|
+|subscriptionPlanId|string|false|none|Handle (slug) of the subscription plan. Links the subscription to rate limit and billing configuration.|
+|status|string|false|none|Subscription status (default ACTIVE)|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|kind|RestApi|
+|kind|LlmProvider|
+|kind|LlmProxy|
+|kind|Mcp|
+|status|ACTIVE|
+|status|INACTIVE|
+|status|REVOKED|
+
+
Subscription
+
+
+
+
+
+
+```json
+{
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "artifactId": "my-rest-api",
+ "kind": "RestApi",
+ "subscriberId": "string",
+ "applicationId": "my-app-handle",
+ "subscriptionToken": "string",
+ "subscriptionPlanId": "gold",
+ "subscriptionPlanName": "string",
+ "organizationId": "acme",
+ "status": "ACTIVE",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string(uuid)|false|none|Subscription ID|
+|artifactId|string|false|none|Handle (ID) of the subscribed artifact|
+|kind|string|false|none|Type of the subscribed artifact|
+|subscriberId|string|false|none|Unique subscriber identifier for this API (required)|
+|applicationId|string|false|none|Handle (ID) of the application this subscription belongs to (optional for token-based subscriptions)|
+|subscriptionToken|string|false|none|Opaque subscription token for API invocation via Subscription-Key header|
+|subscriptionPlanId|string|false|none|Handle (slug) of the subscription plan|
+|subscriptionPlanName|string|false|none|Subscription plan display name (e.g. Bronze, Gold)|
+|organizationId|string|false|none|Handle (URL-friendly slug) of the organization this subscription belongs to|
+|status|string|false|none|none|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource. Only present in the detail response (GET /subscriptions/{subscriptionId}), omitted from list responses.|
+|createdAt|string(date-time)|false|none|none|
+|updatedAt|string(date-time)|false|none|none|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|kind|RestApi|
+|kind|LlmProvider|
+|kind|LlmProxy|
+|kind|Mcp|
+|status|ACTIVE|
+|status|INACTIVE|
+|status|REVOKED|
+
+
+
+
+
+
+
+
+```json
+{
+ "status": "error",
+ "code": "REST_API_NOT_FOUND",
+ "message": "The requested REST API could not be found.",
+ "errors": [
+ {
+ "field": "",
+ "message": "must start with /"
+ }
+ ],
+ "details": {},
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+
+```
+
+Standard error response
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|status|string|true|none|Always the literal "error".|
+|code|string|true|none|Stable, machine-readable error code from the error catalog, in the form `_` (e.g. `REST_API_NOT_FOUND`). Clients and agents should branch on this, not on the HTTP status.|
+|message|string|true|none|Human-readable description of the error.|
+|errors|[[FieldError](#schemafielderror)]|false|none|Per-field validation failures. Present when the error is a validation failure.|
+|details|object|false|none|Optional structured metadata specific to this error condition (e.g. the resources referencing a secret that blocked its deletion). Shape varies by `code`; absent when not applicable.|
+|trackingId|string(uuid)|false|none|Correlation ID for server-side failures. Present only on 5xx responses; quote it when reporting the error so operators can find the corresponding server log entry.|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|error|
+
+
FieldError
+
+
+
+
+
+
+```json
+{
+ "field": "",
+ "message": "must start with /"
+}
+
+```
+
+Field-level validation error
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|field|string|true|none|Path of the offending field.|
+|message|string|true|none|Why the field failed validation.|
+
+
DeployRequest
+
+
+
+
+
+
+```json
+{
+ "name": "v1.0-production",
+ "base": "current",
+ "gatewayId": "prod-gateway-01",
+ "metadata": {}
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|name|string|true|none|Name/label for this deployment (e.g., "v1.0-prod", "hotfix-2024-01-15")|
+|base|string|true|none|The source for the API definition. Can be "current" (latest working copy) or a deploymentId (existing deployment)|
+|gatewayId|string|true|none|Handle (URL-friendly slug) of the target gateway for this deployment|
+|metadata|object|false|none|Optional metadata for the deployment. Supported keys include `endpointUrl`, `vhostMain`, and `vhostSandbox`.|
+
+
DeploymentResponse
+
+
+
+
+
+
+```json
+{
+ "deploymentId": "a73c85a1-d857-491e-a6b2-51dce05de7a2",
+ "name": "v1.0-production",
+ "gatewayId": "prod-gateway-01",
+ "status": "DEPLOYED",
+ "baseDeploymentId": "be6d8692-b9de-400e-b6c1-14db50154e27",
+ "metadata": {},
+ "createdAt": "2019-08-24T14:15:22Z",
+ "statusReason": "string",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|deploymentId|string(uuid)|true|none|Unique identifier for the deployment|
+|name|string|true|none|Name/label for this deployment|
+|gatewayId|string|true|none|Handle (URL-friendly slug) of the gateway|
+|status|string|true|none|Current deployment lifecycle state: - DEPLOYED: Currently active on the gateway - UNDEPLOYED: Suspended but can be rolled back - DEPLOYING: Deployment in progress, waiting for gateway acknowledgement - UNDEPLOYING: Undeployment in progress, waiting for gateway acknowledgement - FAILED: Deployment or undeployment failed (see statusReason for error code) - ARCHIVED: Historical deployment, can be rolled back|
+|baseDeploymentId|string(uuid)¦null|false|none|UUID of the base deployment this was created from|
+|metadata|object|false|none|Metadata associated with the deployment|
+|createdAt|string(date-time)|true|none|Timestamp when the deployment artifact was created|
+|statusReason|string¦null|false|none|Error code explaining the failure reason. Null unless status is FAILED (e.g. DEPLOYMENT_TIMEOUT, GATEWAY_PROCESSING_ERROR)|
+|updatedAt|string(date-time)¦null|false|none|Timestamp when the deployment status last changed (null for ARCHIVED deployments)|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|DEPLOYED|
+|status|UNDEPLOYED|
+|status|DEPLOYING|
+|status|UNDEPLOYING|
+|status|FAILED|
+|status|ARCHIVED|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "openai",
+ "groupId": "openai",
+ "displayName": "OpenAI",
+ "managedBy": "wso2",
+ "description": "Default OpenAI template",
+ "createdBy": "john.doe",
+ "version": "v1.0",
+ "isLatest": true,
+ "enabled": true,
+ "logoUrl": "https://cdn.example.com/logos/openai.svg",
+ "createdAt": "2025-11-25T10:30:00Z",
+ "updatedAt": "2025-11-25T10:30:00Z",
+ "readOnly": false
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Unique handle for this specific template version.|
+|groupId|string|false|read-only|Stable identifier shared by every version of a template family (defaults to the first version's handle). Read-only.|
+|displayName|string|true|none|Human-readable name for the LLM provider template|
+|managedBy|string|false|none|Who manages the template ('wso2' for built-in, otherwise custom-defined).|
+|description|string|false|none|none|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|version|string|false|none|Content version, matching the v. pattern (e.g. v1.0, v2.0).|
+|isLatest|boolean|false|none|Whether this is the latest version of the template.|
+|enabled|boolean|false|none|Whether this version is offered when creating providers.|
+|logoUrl|string(uri)|false|none|URL of the provider logo|
+|createdAt|string(date-time)|false|none|none|
+|updatedAt|string(date-time)|false|none|none|
+|readOnly|boolean|false|none|True when the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane.|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "wso2-openai-provider",
+ "displayName": "WSO2 OpenAI Provider",
+ "description": "Primary OpenAI provider",
+ "createdBy": "john.doe",
+ "readOnly": false,
+ "updatedBy": "john.doe",
+ "version": "v1.0",
+ "context": "/openai",
+ "vhost": "api.openai.com",
+ "template": "openai",
+ "openapi": "openapi: 3.0.3\ninfo:\n title: Provider API\n version: v1.0\npaths: {}\n",
+ "modelProviders": [
+ {
+ "id": "claude",
+ "displayName": "Claude",
+ "models": [
+ {
+ "id": "claude-3-5-sonnet",
+ "displayName": "Claude 3.5 Sonnet",
+ "description": "High-quality reasoning model"
+ }
+ ]
+ }
+ ],
+ "upstream": {
+ "main": {
+ "url": "http://prod-backend:5000/api/v2",
+ "auth": {
+ "type": "api-key",
+ "header": "X-API-Key",
+ "value": "my-api-key-value"
+ }
+ },
+ "sandbox": {
+ "url": "http://prod-backend:5000/api/v2",
+ "auth": {
+ "type": "api-key",
+ "header": "X-API-Key",
+ "value": "my-api-key-value"
+ }
+ }
+ },
+ "accessControl": {
+ "mode": "deny_all",
+ "exceptions": [
+ {
+ "path": "/chat/completions",
+ "methods": [
+ "GET"
+ ]
+ }
+ ]
+ },
+ "rateLimiting": {
+ "providerLevel": {
+ "global": {
+ "request": {
+ "enabled": true,
+ "count": 1500,
+ "reset": {
+ "duration": 2,
+ "unit": "week"
+ }
+ },
+ "token": {
+ "enabled": true,
+ "count": 1000000,
+ "reset": {
+ "duration": 1,
+ "unit": "month"
+ }
+ }
+ }
+ },
+ "consumerLevel": {
+ "resourceWise": {
+ "default": {
+ "request": {
+ "enabled": true,
+ "count": 50,
+ "reset": {
+ "duration": 2,
+ "unit": "week"
+ }
+ },
+ "cost": {
+ "enabled": true,
+ "amount": 100,
+ "reset": {
+ "duration": 1,
+ "unit": "month"
+ }
+ }
+ },
+ "resources": [
+ {
+ "resource": "/models",
+ "limit": {
+ "request": {
+ "enabled": true,
+ "count": 200,
+ "reset": {
+ "duration": 1,
+ "unit": "week"
+ }
+ },
+ "token": {
+ "enabled": true,
+ "count": 100000,
+ "reset": {
+ "duration": 1,
+ "unit": "month"
+ }
+ }
+ }
+ },
+ {
+ "resource": "/chat/completions",
+ "limit": {
+ "request": {
+ "enabled": true,
+ "count": 25,
+ "reset": {
+ "duration": 1,
+ "unit": "week"
+ }
+ },
+ "cost": {
+ "enabled": true,
+ "amount": 10,
+ "reset": {
+ "duration": 1,
+ "unit": "month"
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ "globalPolicies": [
+ {
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+ }
+ ],
+ "operationPolicies": [
+ {
+ "name": "token-based-ratelimit",
+ "version": "v1",
+ "executionCondition": "string",
+ "paths": [
+ {
+ "path": "/chat/completions",
+ "methods": [
+ "GET"
+ ],
+ "params": {}
+ }
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "budgetControl",
+ "version": "v1",
+ "paths": [
+ {
+ "path": "/chat/completions",
+ "methods": [
+ "GET"
+ ],
+ "params": {}
+ }
+ ]
+ }
+ ],
+ "security": {
+ "enabled": true,
+ "apiKey": {
+ "enabled": true,
+ "key": "X-API-Key",
+ "valuePrefix": "Bearer",
+ "in": "header"
+ }
+ },
+ "associatedGateways": [
+ {
+ "id": "prod-eu",
+ "configurations": {
+ "host": "prod-eu.platform-gw.local"
+ }
+ },
+ {
+ "id": "prod-us",
+ "configurations": {
+ "host": "prod-us.platform-gw.local"
+ }
+ }
+ ],
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Unique handle for the provider|
+|displayName|string|true|none|Human-readable LLM Provider name|
+|description|string|false|none|Description of the LLM provider|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|readOnly|boolean|false|read-only|True if the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane; false for control-plane created artifacts.|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource. Only present in the detail response (GET /llm-providers/{id}), omitted from list responses.|
+|version|string|true|none|Semantic version of the LLM Provider|
+|context|string|false|none|Base path for all routes exposed by this provider. Must start with / and carry no trailing slash; the single exception is the root path "/", which is the default.|
+|vhost|string|false|none|Virtual host name used for routing. Supports standard domain names, subdomains, or wildcard domains. Must follow RFC-compliant hostname rules. Wildcards are only allowed in the left-most label (e.g., *.example.com).|
+|template|string|true|none|Template name to use for this LLM Provider|
+|openapi|string|false|none|OpenAPI specification (JSON or YAML) for the provider endpoint|
+|modelProviders|[[LLMModelProvider](#schemallmmodelprovider)]|false|none|List of model providers and their models supported by this provider. For non-aggregator templates, only a single model provider is allowed (aggregator templates: awsbedrock, azureaifoundry).|
+|upstream|[Upstream](#schemaupstream)|true|none|Upstream backend configuration with main and sandbox endpoints|
+|accessControl|[LLMAccessControl](#schemallmaccesscontrol)|true|none|none|
+|rateLimiting|[LLMRateLimitingConfig](#schemallmratelimitingconfig)|false|none|Rate limiting configuration for an LLM provider at provider and consumer levels.|
+|globalPolicies|[[Policy](#schemapolicy)]|false|none|Global (api-level) policies applied across ALL operations as one shared scope, evaluated before operation-level policies.|
+|operationPolicies|[[OperationPolicy](#schemaoperationpolicy)]|false|none|Operation-level policies scoped to specific paths/methods, evaluated after global policies.|
+|policies|[[LLMPolicy](#schemallmpolicy)]|false|none|DEPRECATED - use operationPolicies. Still honoured (treated identically to operationPolicies).|
+|security|[SecurityConfig](#schemasecurityconfig)|false|none|Defines security mechanisms (API key, OAuth2) applicable to the API|
+|associatedGateways|[[AssociatedGateway](#schemaassociatedgateway)]|false|none|Optional list of gateways this LLM provider can be deployed to, along with per-gateway configuration overrides. This field is optional; omitting it does not change existing behaviour.|
+|createdAt|string(date-time)|false|read-only|Timestamp when the resource was created|
+|updatedAt|string(date-time)|false|read-only|Timestamp when the resource was last updated|
+
+
AssociatedGateway
+
+
+
+
+
+
+```json
+{
+ "id": "prod-eu",
+ "configurations": {
+ "someKey": "someValue"
+ }
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|true|none|Handle of the gateway this artifact can be deployed to|
+|configurations|object|false|none|Per-gateway configuration overrides for this artifact. This is a free-form object; the supported keys depend on the deployed artifact type.|
+
+
LLMProviderListItem
+
+
+
+
+
+
+```json
+{
+ "id": "wso2-openai-provider",
+ "displayName": "WSO2 OpenAI Provider",
+ "description": "Primary OpenAI provider",
+ "createdBy": "john.doe",
+ "readOnly": false,
+ "version": "v1.0",
+ "template": "openai",
+ "status": "deployed",
+ "createdAt": "2025-11-25T10:30:00Z",
+ "updatedAt": "2025-11-25T10:30:00Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|none|
+|displayName|string|true|none|Human-readable name for the LLM provider|
+|description|string|false|none|none|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|readOnly|boolean|false|none|True when the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane.|
+|version|string|false|none|none|
+|template|string|false|none|none|
+|status|string|false|none|none|
+|createdAt|string(date-time)|false|none|none|
+|updatedAt|string(date-time)|false|none|none|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|pending|
+|status|deployed|
+|status|failed|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "wso2-con-assistant",
+ "displayName": "WSO2 Con Assistant",
+ "description": "Customer support assistant",
+ "createdBy": "john.doe",
+ "readOnly": false,
+ "updatedBy": "john.doe",
+ "version": "v1.0",
+ "projectId": "default-project",
+ "context": "/openai",
+ "vhost": "api.openai.com",
+ "provider": {
+ "id": "wso2-openai-provider",
+ "auth": {
+ "type": "api-key",
+ "header": "X-API-Key",
+ "value": "my-api-key-value"
+ }
+ },
+ "additionalProviders": [
+ {
+ "id": "anthropic-provider",
+ "as": "anthropic-upstream",
+ "transformer": {
+ "type": "openai-to-anthropic",
+ "version": "v1",
+ "params": {}
+ }
+ }
+ ],
+ "openapi": "openapi: 3.0.3\ninfo:\n title: Proxy API\n version: v1.0\npaths: {}\n",
+ "globalPolicies": [
+ {
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+ }
+ ],
+ "operationPolicies": [
+ {
+ "name": "token-based-ratelimit",
+ "version": "v1",
+ "executionCondition": "string",
+ "paths": [
+ {
+ "path": "/chat/completions",
+ "methods": [
+ "GET"
+ ],
+ "params": {}
+ }
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "budgetControl",
+ "version": "v1",
+ "paths": [
+ {
+ "path": "/chat/completions",
+ "methods": [
+ "GET"
+ ],
+ "params": {}
+ }
+ ]
+ }
+ ],
+ "security": {
+ "enabled": true,
+ "apiKey": {
+ "enabled": true,
+ "key": "X-API-Key",
+ "valuePrefix": "Bearer",
+ "in": "header"
+ }
+ },
+ "associatedGateways": [
+ {
+ "id": "prod-eu"
+ },
+ {
+ "id": "prod-us"
+ }
+ ],
+ "createdAt": "2023-10-12T10:30:00Z",
+ "updatedAt": "2023-10-12T10:30:00Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Unique handle for the proxy|
+|displayName|string|true|none|Human-readable LLM proxy name|
+|description|string|false|none|Description of the LLM proxy|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|readOnly|boolean|false|read-only|True if the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane; false for control-plane created artifacts.|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource. Only present in the detail response (GET /llm-proxies/{id}), omitted from list responses.|
+|version|string|true|none|Semantic version of the LLM proxy|
+|projectId|string|true|none|Handle (URL-friendly slug) of the project this proxy belongs to|
+|context|string|false|none|Base path for all routes exposed by this proxy. Must start with / and carry no trailing slash; the single exception is the root path "/", which is the default.|
+|vhost|string|false|none|Virtual host name used for routing. Supports standard domain names, subdomains, or wildcard domains. Must follow RFC-compliant hostname rules. Wildcards are only allowed in the left-most label (e.g., *.example.com).|
+|provider|[LLMProxyProvider](#schemallmproxyprovider)|true|none|none|
+|additionalProviders|[[LLMProxyAdditionalProvider](#schemallmproxyadditionalprovider)]|false|none|Optional list of additional LLM providers attached to this proxy as selectable upstreams. Policies route requests to any of these by setting the upstream name. The primary `provider` field above remains the default upstream and the FK target.|
+|openapi|string|false|none|OpenAPI specification (JSON or YAML) for the proxy endpoint|
+|globalPolicies|[[Policy](#schemapolicy)]|false|none|Global (api-level) policies applied across ALL operations as one shared scope, evaluated before operation-level policies.|
+|operationPolicies|[[OperationPolicy](#schemaoperationpolicy)]|false|none|Operation-level policies scoped to specific paths/methods, evaluated after global policies.|
+|policies|[[LLMPolicy](#schemallmpolicy)]|false|none|DEPRECATED - use operationPolicies. Still honoured (treated identically to operationPolicies).|
+|security|[SecurityConfig](#schemasecurityconfig)|false|none|Defines security mechanisms (API key, OAuth2) applicable to the API|
+|associatedGateways|[[AssociatedGateway](#schemaassociatedgateway)]|false|none|Optional list of gateways this LLM proxy can be deployed to, along with per-gateway configuration overrides. This field is optional; omitting it does not change existing behaviour.|
+|createdAt|string(date-time)|false|read-only|Timestamp when the resource was created|
+|updatedAt|string(date-time)|false|read-only|Timestamp when the resource was last updated|
+
+
LLMProxyListItem
+
+
+
+
+
+
+```json
+{
+ "id": "wso2-con-assistant",
+ "displayName": "WSO2 Con Assistant",
+ "description": "Customer support assistant",
+ "createdBy": "john.doe",
+ "context": "/wso2-con-assistant",
+ "version": "v1.0",
+ "projectId": "default-project",
+ "provider": "wso2-openai-provider",
+ "status": "deployed",
+ "createdAt": "2025-11-25T10:30:00Z",
+ "updatedAt": "2025-11-25T10:30:00Z",
+ "readOnly": false
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|none|
+|displayName|string|true|none|Human-readable name for the LLM proxy|
+|description|string|false|none|none|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|context|string|false|none|Context path where the proxy is exposed|
+|version|string|false|none|none|
+|projectId|string|false|none|Handle (URL-friendly slug) of the project this proxy belongs to|
+|provider|string|false|none|Unique id of a deployed llm provider|
+|status|string|false|none|none|
+|createdAt|string(date-time)|false|none|none|
+|updatedAt|string(date-time)|false|none|none|
+|readOnly|boolean|false|none|True when the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane.|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|pending|
+|status|deployed|
+|status|failed|
+
+
+
+
+
+
+
+
+```json
+{
+ "id": "anthropic-provider",
+ "as": "anthropic-upstream",
+ "transformer": {
+ "type": "openai-to-anthropic",
+ "version": "v1",
+ "params": {}
+ }
+}
+
+```
+
+Additional LLM provider attached to this proxy as a selectable upstream. Policies route to it by referring to the `as` name (defaults to `id`).
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|true|none|Unique id of a deployed llm provider|
+|as|string|false|none|Logical LLM Provider name used by policies to select this provider. Must be unique within the proxy. Defaults to `id` when omitted.|
+|transformer|[LLMProxyTransformer](#schemallmproxytransformer)|false|none|Request/response translator applied when this provider is the selected upstream. The proxy injects the translator as a conditional policy whose execution condition matches this provider, so it runs only when the provider is selected. The provider's `as` name (defaults to `id`) is passed to the translator as its target upstream.|
+
+
LLMProxyTransformer
+
+
+
+
+
+
+```json
+{
+ "type": "openai-to-anthropic",
+ "version": "v1",
+ "params": {}
+}
+
+```
+
+Request/response translator applied when this provider is the selected upstream. The proxy injects the translator as a conditional policy whose execution condition matches this provider, so it runs only when the provider is selected. The provider's `as` name (defaults to `id`) is passed to the translator as its target upstream.
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|type|string|true|none|Translator policy name (for example openai-to-anthropic).|
+|version|string|true|none|Major-only translator policy version (for example v1). The Gateway Controller resolves it to the installed full version.|
+|params|object|false|none|Translator-specific parameters (for example model, apiVersion).|
+
+
CreateLLMProviderAPIKeyRequest
+
+
+
+
+
+
+```json
+{
+ "id": "production-key",
+ "displayName": "Production Key",
+ "expiresAt": "2026-12-31T23:59:59Z",
+ "issuer": "api-platform-devportal",
+ "allowedTargets": "dev_gateway,test_gateway"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Unique identifier for the API key within the LLM provider. If not provided, generated from displayName.|
+|displayName|string|true|none|Human-readable name for the API key|
+|expiresAt|string(date-time)|false|none|Optional expiration time in ISO 8601 format|
+|issuer|string¦null|false|none|Identifier of the developer portal that provisioned this API key. Null if not provided.|
+|allowedTargets|string¦null|false|none|Comma-separated list of gateways this key is valid for. Use 'ALL' to allow all targets (default).|
+
+
CreateLLMProviderAPIKeyResponse
+
+
+
+
+
+
+```json
+{
+ "status": "success",
+ "message": "API key created and broadcasted to gateways successfully",
+ "id": "production-key",
+ "apiKey": "REDACTED_API_KEY"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|status|string|true|none|Status of the operation|
+|message|string|true|none|Detailed message about the operation result|
+|id|string|true|none|Unique identifier of the generated key|
+|apiKey|string|true|none|The generated API key value — 64 hexadecimal characters, returned only in this creation response and never retrievable afterwards. The example value is a non-functional placeholder.|
+
+
CreateLLMProxyAPIKeyRequest
+
+
+
+
+
+
+```json
+{
+ "id": "production-key",
+ "displayName": "Production Key",
+ "expiresAt": "2026-12-31T23:59:59Z",
+ "issuer": "api-platform-devportal",
+ "allowedTargets": "dev_gateway,test_gateway"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Unique identifier for the API key within the LLM proxy. If not provided, generated from displayName.|
+|displayName|string|true|none|Human-readable name for the API key|
+|expiresAt|string(date-time)|false|none|Optional expiration time in ISO 8601 format|
+|issuer|string¦null|false|none|Identifier of the developer portal that provisioned this API key. Null if not provided.|
+|allowedTargets|string¦null|false|none|Comma-separated list of gateways this key is valid for. Use 'ALL' to allow all targets (default).|
+
+
CreateLLMProxyAPIKeyResponse
+
+
+
+
+
+
+```json
+{
+ "status": "success",
+ "message": "API key created and broadcasted to gateways successfully",
+ "id": "production-key",
+ "apiKey": "REDACTED_API_KEY"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|status|string|true|none|Status of the operation|
+|message|string|true|none|Detailed message about the operation result|
+|id|string|true|none|Unique identifier of the generated key|
+|apiKey|string|true|none|The generated API key value — 64 hexadecimal characters, returned only in this creation response and never retrievable afterwards. The example value is a non-functional placeholder.|
+
+
MCPProxy
+
+
+
+
+
+
+```json
+{
+ "id": "weather-mcp-proxy",
+ "displayName": "Weather Server",
+ "description": "An MCP server which provides weather information",
+ "createdBy": "john.doe",
+ "readOnly": false,
+ "updatedBy": "john.doe",
+ "version": "v1.0",
+ "projectId": "default-project",
+ "context": "/",
+ "vhost": "mcp.gw.com",
+ "upstream": {
+ "main": {
+ "url": "http://prod-backend:5000/api/v2",
+ "auth": {
+ "type": "api-key",
+ "header": "X-API-Key",
+ "value": "my-api-key-value"
+ }
+ },
+ "sandbox": {
+ "url": "http://prod-backend:5000/api/v2",
+ "auth": {
+ "type": "api-key",
+ "header": "X-API-Key",
+ "value": "my-api-key-value"
+ }
+ }
+ },
+ "mcpSpecVersion": "2025-06-18",
+ "policies": [
+ {
+ "executionCondition": "request.header.x-custom == 'enabled'",
+ "name": "SET_HEADER",
+ "params": {
+ "key": "MyHeader",
+ "value": "MyValue"
+ },
+ "version": "v1"
+ }
+ ],
+ "kind": "Mcp",
+ "capabilities": {
+ "tools": [
+ {}
+ ],
+ "resources": [
+ {}
+ ],
+ "prompts": [
+ {}
+ ]
+ },
+ "associatedGateways": [
+ {
+ "id": "prod-eu"
+ },
+ {
+ "id": "prod-us"
+ }
+ ],
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Unique handle for the proxy|
+|displayName|string|true|none|Human-readable MCP proxy name|
+|description|string|false|none|Description of the MCP proxy|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|readOnly|boolean|false|read-only|True if the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane; false for control-plane created artifacts.|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource. Only present in the detail response (GET /mcp-proxies/{id}), omitted from list responses.|
+|version|string|true|none|Semantic version of the MCP proxy|
+|projectId|string|false|none|Handle (URL-friendly slug) of the project this proxy belongs to|
+|context|string|false|none|Base path for all routes exposed by this proxy. Must start with / and carry no trailing slash; the single exception is the root path "/", which is the default.|
+|vhost|string|false|none|Virtual host name used for routing. Supports standard domain names, subdomains, or wildcard domains. Must follow RFC-compliant hostname rules. Wildcards are only allowed in the left-most label (e.g., *.example.com).|
+|upstream|[Upstream](#schemaupstream)|true|none|Upstream backend configuration with main and sandbox endpoints|
+|mcpSpecVersion|string|false|none|MCP specification version supported by this proxy|
+|policies|[[Policy](#schemapolicy)]|false|none|List of policies to be applied|
+|kind|string|false|none|Kind of the API based on its communication protocol or architectural style|
+|capabilities|[MCPProxyCapabilities](#schemamcpproxycapabilities)|false|none|List of capabilities supported by this proxy. This will be stored as-is and can be used in the future if we need this for governance purposes|
+|associatedGateways|[[AssociatedGateway](#schemaassociatedgateway)]|false|none|Optional list of gateways this MCP proxy can be deployed to, along with per-gateway configuration overrides. This field is optional; omitting it does not change existing behaviour.|
+|createdAt|string(date-time)|false|read-only|Timestamp when the resource was created|
+|updatedAt|string(date-time)|false|read-only|Timestamp when the resource was last updated|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|mcpSpecVersion|2025-06-18|
+|mcpSpecVersion|2025-11-25|
+
+
MCPProxyListItem
+
+
+
+
+
+
+```json
+{
+ "id": "weather-mcp-proxy",
+ "displayName": "Weather Server",
+ "description": "An MCP server which provides weather information",
+ "createdBy": "john.doe",
+ "context": "/weather-mcp-proxy",
+ "version": "v1.0",
+ "projectId": "default-project",
+ "status": "deployed",
+ "mcpSpecVersion": "2025-11-25",
+ "createdAt": "2025-11-25T10:30:00Z",
+ "updatedAt": "2025-11-25T10:30:00Z",
+ "readOnly": false
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|none|
+|displayName|string|true|none|Human-readable name for the MCP proxy|
+|description|string|false|none|none|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|context|string|false|none|Context path where the proxy is exposed|
+|version|string|false|none|none|
+|projectId|string|false|none|Handle (URL-friendly slug) of the project this proxy belongs to|
+|status|string|false|none|none|
+|mcpSpecVersion|string|false|none|none|
+|createdAt|string(date-time)|false|none|none|
+|updatedAt|string(date-time)|false|none|none|
+|readOnly|boolean|false|none|True when the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane.|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|status|pending|
+|status|deployed|
+|status|failed|
+
+
+
+
+
+
+
+
+```json
+{
+ "url": "https://mcp.server.com/mcp",
+ "proxyId": "my-mcp-proxy",
+ "auth": {
+ "type": "api-key",
+ "header": "X-API-Key",
+ "value": "my-api-key-value"
+ }
+}
+
+```
+
+Target MCP server to introspect. Provide exactly one of `url` (a direct backend URL,
+optionally with `auth`) or `proxyId` (refetch using a stored proxy configuration) — never
+both. `auth` must not be sent together with `proxyId`; the stored auth is always used in
+the refetch flow.
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|url|string(uri)|false|none|Endpoint URL of the MCP server to fetch information from. Mutually exclusive with `proxyId`; exactly one of the two must be provided.|
+|proxyId|string|false|none|MCP proxy handle (identifier) for refresh operations. When provided, the server fetches URL and auth from the stored proxy configuration. Mutually exclusive with `url`; exactly one of the two must be provided.|
+|auth|[UpstreamAuth](#schemaupstreamauth)|false|none|Authentication configuration for the fetch request. Allowed only alongside `url` (initial creation flow); sending it with `proxyId` is rejected, as the stored auth is used in the refetch flow.|
+
+oneOf
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|object|false|none|none|
+
+xor
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|object|false|none|none|
+
+not
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|» *anonymous*|object|false|none|none|
+
+
+
+
+
+
+
+
+```json
+{
+ "tools": [
+ {}
+ ],
+ "resources": [
+ {}
+ ],
+ "prompts": [
+ {}
+ ]
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|tools|[object]|false|none|List of tool capabilities supported by this proxy|
+|resources|[object]|false|none|List of resource capabilities supported by this proxy|
+|prompts|[object]|false|none|List of prompt capabilities supported by this proxy|
+
+
SecretCreateRequest
+
+
+
+
+
+
+```json
+{
+ "id": "wso2-openai-key",
+ "displayName": "WSO2 OpenAI API Key",
+ "description": "Primary API key for WSO2 OpenAI integration",
+ "value": "sk-xxx",
+ "type": "GENERIC"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Handle (slug) used in {{ secret "id" }} placeholders. Immutable after creation.|
+|displayName|string|true|none|Human-readable name for the secret|
+|description|string|false|none|none|
+|value|string|true|write-only|Plaintext secret value — encrypted at rest, never returned in any response|
+|type|string|false|none|none|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|type|GENERIC|
+|type|CERTIFICATE|
+
+
SecretUpdateRequest
+
+
+
+
+
+
+```json
+{
+ "id": "wso2-openai-key",
+ "displayName": "string",
+ "description": "string",
+ "value": "string"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|Secret handle — if provided, must match the path parameter; returns 400 if they differ. The handle is immutable and cannot be changed via update.|
+|displayName|string|true|none|Human-readable name for the secret|
+|description|string|false|none|none|
+|value|string|true|write-only|New plaintext secret value — re-encrypted at rest|
+
+
SecretResponse
+
+
+
+
+
+
+```json
+{
+ "id": "wso2-openai-key",
+ "displayName": "WSO2 OpenAI API Key",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+
+```
+
+Returned on create (201) and rotate (200). The plaintext value is never included.
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|none|
+|displayName|string|true|none|Human-readable name for the secret|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|updatedBy|string|false|read-only|User identifier of the user who last updated this resource|
+|createdAt|string(date-time)|false|none|none|
+|updatedAt|string(date-time)|false|none|none|
+
+
SecretSummary
+
+
+
+
+
+
+```json
+{
+ "id": "wso2-openai-key",
+ "displayName": "WSO2 OpenAI API Key",
+ "description": "string",
+ "type": "GENERIC",
+ "provider": "IN_BUILT",
+ "status": "ACTIVE",
+ "hash": "hmac-sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe04294e576d4b3d4c57e3f428a",
+ "createdBy": "john.doe",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+
+```
+
+Secret metadata — never includes the plaintext value.
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|none|
+|displayName|string|true|none|Human-readable name for the secret|
+|description|string|false|none|none|
+|type|string|false|none|none|
+|provider|string|false|none|none|
+|status|string|false|none|none|
+|hash|string|false|none|none|
+|createdBy|string|false|read-only|User identifier of the user who created this resource|
+|createdAt|string(date-time)|false|none|none|
+|updatedAt|string(date-time)|false|none|none|
+
+#### Enumerated Values
+
+|Property|Value|
+|---|---|
+|type|GENERIC|
+|type|CERTIFICATE|
+|provider|IN_BUILT|
+|status|ACTIVE|
+|status|DEPRECATED|
+
+
+
+Encrypted secret management — create, rotate, and delete organization-scoped secrets referenced via {{ secret "name" }} placeholders
+
+## Create a secret
+
+
+
+`POST /secrets`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/secrets \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: multipart/form-data' \
+ -H 'Accept: application/json' \
+ -F 'id=wso2-openai-key' \
+ -F 'displayName=WSO2 OpenAI API Key' \
+ -F 'description=Primary API key for WSO2 OpenAI integration' \
+ -F 'value=sk-xxx' \
+ -F 'type=GENERIC'
+
+```
+
+Create a new encrypted secret scoped to the organization. The plaintext value is never returned.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|body|body|[SecretCreateRequest](schemas.md#schemasecretcreaterequest)|true|none|
+
+> Example responses
+>
+> 201 Response
+
+```json
+{
+ "id": "wso2-openai-key",
+ "displayName": "WSO2 OpenAI API Key",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+> 503 Response
+
+```json
+{
+ "status": "error",
+ "code": "SERVICE_UNAVAILABLE",
+ "message": "Secrets management is not configured.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Secret created successfully.|[SecretResponse](schemas.md#schemasecretresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. The secrets management feature is not configured.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List secrets
+
+
+
+`GET /secrets`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/secrets \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns metadata for all secrets in the organization. The plaintext value is
+never included in list or get responses.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+|updatedAfter|query|string(date-time)|false|RFC3339 timestamp — return only secrets updated after this time. Used by GW controller for incremental polling.|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "count": 0,
+ "list": [
+ {
+ "id": "wso2-openai-key",
+ "displayName": "WSO2 OpenAI API Key",
+ "description": "string",
+ "type": "GENERIC",
+ "provider": "IN_BUILT",
+ "status": "ACTIVE",
+ "hash": "hmac-sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe04294e576d4b3d4c57e3f428a",
+ "createdBy": "john.doe",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+ }
+ ],
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+> 503 Response
+
+```json
+{
+ "status": "error",
+ "code": "SERVICE_UNAVAILABLE",
+ "message": "Secrets management is not configured.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of secret metadata|[SecretListResponse](schemas.md#schemasecretlistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. The secrets management feature is not configured.|[Error](schemas.md#schemaerror)|
+
+## Get a secret by handle
+
+
+
+`GET /secrets/{secretId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/secrets/{secretId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns metadata for a single secret. The plaintext value is never returned.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Secret rotated successfully.|[SecretResponse](schemas.md#schemasecretresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. The secrets management feature is not configured.|[Error](schemas.md#schemaerror)|
+
+## Delete a secret
+
+
+
+`DELETE /secrets/{secretId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/secrets/{secretId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Soft-deletes a secret by marking it as DEPRECATED. Returns 409 if the secret is
+still referenced by any LLM provider or API configuration.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|secretId|path|string|true|The secret handle|
+
+> Example responses
+>
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 409 Response
+
+```json
+{
+ "status": "error",
+ "code": "SECRET_IN_USE",
+ "message": "The secret is referenced by one or more active resources.",
+ "details": {
+ "references": [
+ {
+ "type": "llm_provider",
+ "handle": "wso2-openai-provider",
+ "name": "WSO2 OpenAI Provider"
+ }
+ ]
+ }
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+> 503 Response
+
+```json
+{
+ "status": "error",
+ "code": "SERVICE_UNAVAILABLE",
+ "message": "Secrets management is not configured.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Secret deleted successfully|None|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The secret is referenced by one or more active resources.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+|503|[Service Unavailable](https://tools.ietf.org/html/rfc7231#section-6.6.4)|Service Unavailable. The secrets management feature is not configured.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/subscriptionplans.md b/docs/rest-apis/platform-api/subscriptionplans.md
new file mode 100644
index 0000000000..9c0390dfcc
--- /dev/null
+++ b/docs/rest-apis/platform-api/subscriptionplans.md
@@ -0,0 +1,610 @@
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Subscription plan created successfully|[SubscriptionPlan](schemas.md#schemasubscriptionplan)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List subscription plans
+
+
+
+`GET /subscription-plans`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/subscription-plans \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns subscription plans for the organization.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Updated subscription plan|[SubscriptionPlan](schemas.md#schemasubscriptionplan)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete subscription plan
+
+
+
+`DELETE /subscription-plans/{subscriptionPlanId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/subscription-plans/{subscriptionPlanId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|subscriptionPlanId|path|string|true|none|
+
+> Example responses
+>
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Subscription plan deleted (no content)|None|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/docs/rest-apis/platform-api/subscriptions.md b/docs/rest-apis/platform-api/subscriptions.md
new file mode 100644
index 0000000000..890c015d2b
--- /dev/null
+++ b/docs/rest-apis/platform-api/subscriptions.md
@@ -0,0 +1,675 @@
+
Subscriptions
+
+Subscription management operations linking applications to APIs under a subscription plan
+
+## Create subscription
+
+
+
+`POST /subscriptions`
+
+> Code samples
+
+```shell
+
+curl -X POST https://localhost:9243/api/v0.9/subscriptions \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -d @payload.json
+
+```
+
+Creates a subscription for the specified artifact.
+`subscriberId` identifies the unique subscriber for this artifact, allowing multiple subscribers to create subscriptions for the same artifact.
+
+> Payload
+
+```json
+{
+ "artifactId": "my-rest-api",
+ "kind": "RestApi",
+ "subscriberId": "user-123",
+ "applicationId": "my-app-handle",
+ "subscriptionPlanId": "gold",
+ "status": "ACTIVE"
+}
+```
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Subscription created successfully|[Subscription](schemas.md#schemasubscription)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request conflicts with the current state of the resource.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+### Response Headers
+
+|Status|Header|Type|Format|Description|
+|---|---|---|---|---|
+|201|Location|string|uri|URL of the newly created resource.|
+
+## List subscriptions
+
+
+
+`GET /subscriptions`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/subscriptions \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns subscriptions filtered by artifact and/or application.
+Optional query parameters artifactId, subscriberId, applicationId and status filter the list.
+Supports pagination via limit and offset.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|artifactId|query|string|false|Filter by artifact ID (UUID or handle)|
+|subscriberId|query|string|false|Filter by subscriber ID|
+|applicationId|query|string|false|Filter by application ID|
+|status|query|string|false|Filter by status (ACTIVE, INACTIVE, REVOKED)|
+|limit|query|integer|false|Maximum number of items to return per page.|
+|offset|query|integer|false|Zero-based index of the first item to return.|
+
+#### Enumerated Values
+
+|Parameter|Value|
+|---|---|
+|status|ACTIVE|
+|status|INACTIVE|
+|status|REVOKED|
+
+> Example responses
+>
+> 200 Response
+
+```json
+{
+ "list": [
+ {
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "artifactId": "my-rest-api",
+ "kind": "RestApi",
+ "subscriberId": "string",
+ "applicationId": "my-app-handle",
+ "subscriptionToken": "string",
+ "subscriptionPlanId": "gold",
+ "subscriptionPlanName": "string",
+ "organizationId": "acme",
+ "status": "ACTIVE",
+ "createdBy": "john.doe",
+ "updatedBy": "john.doe",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "updatedAt": "2019-08-24T14:15:22Z"
+ }
+ ],
+ "count": 0,
+ "pagination": {
+ "total": 10,
+ "offset": 0,
+ "limit": 10
+ }
+}
+```
+
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|List of subscriptions|[SubscriptionListResponse](schemas.md#schemasubscriptionlistresponse)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Get subscription by ID
+
+
+
+`GET /subscriptions/{subscriptionId}`
+
+> Code samples
+
+```shell
+
+curl -X GET https://localhost:9243/api/v0.9/subscriptions/{subscriptionId} \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Returns a single subscription by ID, scoped to the organization in the access token.
+
+### Authentication
+
+
+
+
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Updated subscription|[Subscription](schemas.md#schemasubscription)|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
+
+## Delete subscription
+
+
+
+`DELETE /subscriptions/{subscriptionId}`
+
+> Code samples
+
+```shell
+
+curl -X DELETE https://localhost:9243/api/v0.9/subscriptions/{subscriptionId}?subscriberId=string \
+ -H 'Authorization: Bearer {access_token}' \
+ -H 'Accept: application/json'
+
+```
+
+Removes the subscription for the API.
+Query parameter `subscriberId` is required and must match the subscription's subscriber for access control.
+
+### Authentication
+
+
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|subscriptionId|path|string(uuid)|true|Subscription UUID|
+|subscriberId|query|string|true|Subscriber ID; must match the subscription's subscriberId.|
+
+> Example responses
+>
+> 400 Response
+
+```json
+{
+ "status": "error",
+ "code": "VALIDATION_FAILED",
+ "message": "The request failed validation.",
+ "errors": [
+ {
+ "field": "",
+ "message": ""
+ }
+ ]
+}
+```
+
+> 401 Response
+
+```json
+{
+ "status": "error",
+ "code": "UNAUTHORIZED",
+ "message": "Authorization header is required, or the token is invalid or expired."
+}
+```
+
+> 403 Response
+
+```json
+{
+ "status": "error",
+ "code": "FORBIDDEN",
+ "message": "You do not have permission to perform this action."
+}
+```
+
+> 404 Response
+
+```json
+{
+ "status": "error",
+ "code": "NOT_FOUND",
+ "message": "The specified resource does not exist."
+}
+```
+
+> 500 Response
+
+```json
+{
+ "status": "error",
+ "code": "INTERNAL_ERROR",
+ "message": "An unexpected error occurred.",
+ "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Subscription deleted (no content)|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. Invalid request or validation error.|[Error](schemas.md#schemaerror)|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. Authentication credentials are missing or invalid.|[Error](schemas.md#schemaerror)|
+|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The authenticated user does not have permission to access this resource.|[Error](schemas.md#schemaerror)|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found. The specified resource does not exist.|[Error](schemas.md#schemaerror)|
+|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal Server Error.|[Error](schemas.md#schemaerror)|
diff --git a/platform-api/Makefile b/platform-api/Makefile
index f9f1644d9c..26bcb8104e 100644
--- a/platform-api/Makefile
+++ b/platform-api/Makefile
@@ -34,7 +34,12 @@ MSSQL_PASSWORD ?= Strong!Passw0rd
# On Apple Silicon, override with: MSSQL_IMAGE=mcr.microsoft.com/azure-sql-edge:latest
MSSQL_IMAGE ?= mcr.microsoft.com/mssql/server:2022-latest
-.PHONY: help build test push build-and-push-multiarch it it-sqlite it-postgres it-sqlserver it-all-dbs
+# Doc generation
+REPO_ROOT := $(shell git rev-parse --show-toplevel)
+APIDOCS_TOOLS_DIR := $(REPO_ROOT)/tools/apidocs
+DOCS_OUT_DIR := $(REPO_ROOT)/docs/rest-apis/platform-api
+
+.PHONY: help build test push build-and-push-multiarch it it-sqlite it-postgres it-sqlserver it-all-dbs generate generate-apidocs
help: ## Show this help message
@echo 'Platform API Build System (Version: $(VERSION))'
@@ -105,6 +110,17 @@ generate: ## Generate API server code from OpenAPI spec
@echo "Generating API models for the eventgateway plugin spec..."
@go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.5.1 --config=oapi-codegen-eventgateway.yaml plugins/eventgateway/openapi.yaml
+generate-apidocs: ## Generate REST API docs (Markdown) from the Platform API OpenAPI spec
+ @command -v widdershins >/dev/null 2>&1 || { echo "widdershins not found, installing..."; npm install -g widdershins; }
+ @echo "Generating REST API docs..."
+ @widdershins resources/openapi.yaml -o /tmp/platform-api-openapi.md \
+ --omitHeader --summary \
+ --language_tabs 'shell:Shell:curl' \
+ --user_templates $(APIDOCS_TOOLS_DIR)/widdershins_templates/
+ @bash $(APIDOCS_TOOLS_DIR)/split-openapi.sh /tmp/platform-api-openapi.md $(DOCS_OUT_DIR)
+ @rm -f /tmp/platform-api-openapi.md
+ @echo "REST API docs generated at $(DOCS_OUT_DIR)"
+
push: ## Push Docker image to registry
@echo "Pushing Docker images..."
docker push $(IMAGE_NAME):$(VERSION)
diff --git a/platform-api/api/generated.go b/platform-api/api/generated.go
index d0b5a2021a..a44ee4f2bd 100644
--- a/platform-api/api/generated.go
+++ b/platform-api/api/generated.go
@@ -788,7 +788,7 @@ type CreateLLMProviderAPIKeyRequest struct {
// CreateLLMProviderAPIKeyResponse defines model for CreateLLMProviderAPIKeyResponse.
type CreateLLMProviderAPIKeyResponse struct {
- // ApiKey The generated API key value (shown only once, 64 hexadecimal characters)
+ // ApiKey The generated API key value — 64 hexadecimal characters, returned only in this creation response and never retrievable afterwards. The example value is a non-functional placeholder.
ApiKey string `binding:"required" json:"apiKey" yaml:"apiKey"`
// Id Unique identifier of the generated key
@@ -853,7 +853,7 @@ type CreateLLMProxyAPIKeyRequest struct {
// CreateLLMProxyAPIKeyResponse defines model for CreateLLMProxyAPIKeyResponse.
type CreateLLMProxyAPIKeyResponse struct {
- // ApiKey The generated API key value (shown only once, 64 hexadecimal characters)
+ // ApiKey The generated API key value — 64 hexadecimal characters, returned only in this creation response and never retrievable afterwards. The example value is a non-functional placeholder.
ApiKey string `binding:"required" json:"apiKey" yaml:"apiKey"`
// Id Unique identifier of the generated key
@@ -1312,7 +1312,7 @@ type LLMProvider struct {
// AssociatedGateways Optional list of gateways this LLM provider can be deployed to, along with per-gateway configuration overrides. This field is optional; omitting it does not change existing behaviour.
AssociatedGateways *[]AssociatedGateway `json:"associatedGateways,omitempty" yaml:"associatedGateways,omitempty"`
- // Context Base path for all REST API routes (must start with /, no trailing slash)
+ // Context Base path for all routes exposed by this proxy. Must start with / and carry no trailing slash; the single exception is the root path "/", which is the default.
Context *string `json:"context,omitempty" yaml:"context,omitempty"`
// CreatedAt Timestamp when the resource was created
@@ -1570,7 +1570,7 @@ type LLMProxy struct {
// AssociatedGateways Optional list of gateways this LLM proxy can be deployed to, along with per-gateway configuration overrides. This field is optional; omitting it does not change existing behaviour.
AssociatedGateways *[]AssociatedGateway `json:"associatedGateways,omitempty" yaml:"associatedGateways,omitempty"`
- // Context Base path for all REST API routes (must start with /, no trailing slash)
+ // Context Base path for all routes exposed by this proxy. Must start with / and carry no trailing slash; the single exception is the root path "/", which is the default.
Context *string `json:"context,omitempty" yaml:"context,omitempty"`
// CreatedAt Timestamp when the resource was created
@@ -1601,7 +1601,7 @@ type LLMProxy struct {
// Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set
Policies *[]LLMPolicy `json:"policies,omitempty" yaml:"policies,omitempty"`
- // ProjectId UUID of the project this proxy belongs to
+ // ProjectId Handle (URL-friendly slug) of the project this proxy belongs to
ProjectId string `binding:"required" json:"projectId" yaml:"projectId"`
Provider LLMProxyProvider `json:"provider" yaml:"provider"`
@@ -1660,7 +1660,7 @@ type LLMProxyListItem struct {
DisplayName string `binding:"required" json:"displayName" yaml:"displayName"`
Id *string `json:"id,omitempty" yaml:"id,omitempty"`
- // ProjectId UUID of the project this proxy belongs to
+ // ProjectId Handle (URL-friendly slug) of the project this proxy belongs to
ProjectId *string `json:"projectId,omitempty" yaml:"projectId,omitempty"`
// Provider Unique id of a deployed llm provider
@@ -1719,7 +1719,7 @@ type MCPProxy struct {
AssociatedGateways *[]AssociatedGateway `json:"associatedGateways,omitempty" yaml:"associatedGateways,omitempty"`
Capabilities *MCPProxyCapabilities `json:"capabilities,omitempty" yaml:"capabilities,omitempty"`
- // Context Base path for all REST API routes (must start with /, no trailing slash)
+ // Context Base path for all routes exposed by this proxy. Must start with / and carry no trailing slash; the single exception is the root path "/", which is the default.
Context *string `json:"context,omitempty" yaml:"context,omitempty"`
// CreatedAt Timestamp when the resource was created
@@ -1746,7 +1746,7 @@ type MCPProxy struct {
// Policies List of policies to be applied
Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"`
- // ProjectId UUID of the project this proxy belongs to
+ // ProjectId Handle (URL-friendly slug) of the project this proxy belongs to
ProjectId *string `json:"projectId,omitempty" yaml:"projectId,omitempty"`
// ReadOnly True if the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane; false for control-plane created artifacts.
@@ -1798,7 +1798,7 @@ type MCPProxyListItem struct {
Id *string `json:"id,omitempty" yaml:"id,omitempty"`
McpSpecVersion *string `json:"mcpSpecVersion,omitempty" yaml:"mcpSpecVersion,omitempty"`
- // ProjectId UUID of the project this proxy belongs to
+ // ProjectId Handle (URL-friendly slug) of the project this proxy belongs to
ProjectId *string `json:"projectId,omitempty" yaml:"projectId,omitempty"`
// ReadOnly True when the artifact originated from a data-plane gateway (origin gateway_api) and is read-only in the control plane.
@@ -2499,7 +2499,7 @@ type TokenRotationResponse struct {
// Message Informational message about token rotation
Message *string `json:"message,omitempty" yaml:"message,omitempty"`
- // Token Plain-text new authentication token (only exposed once during rotation)
+ // Token Plain-text new authentication token (only exposed once during rotation). The example value is a non-functional placeholder.
Token *string `json:"token,omitempty" yaml:"token,omitempty"`
}
@@ -2542,10 +2542,12 @@ type UpdateAPIKeyResponseStatus string
// Upstream Upstream backend configuration with main and sandbox endpoints
type Upstream struct {
- // Main Upstream endpoint configuration (single target or reference)
+ // Main Upstream endpoint configuration. Provide exactly one of `url` (a direct backend URL) or
+ // `ref` (a reference to a predefined upstream definition) — never both.
Main UpstreamDefinition `json:"main" yaml:"main"`
- // Sandbox Upstream endpoint configuration (single target or reference)
+ // Sandbox Upstream endpoint configuration. Provide exactly one of `url` (a direct backend URL) or
+ // `ref` (a reference to a predefined upstream definition) — never both.
Sandbox *UpstreamDefinition `json:"sandbox,omitempty" yaml:"sandbox,omitempty"`
}
@@ -2564,15 +2566,16 @@ type UpstreamAuth struct {
// UpstreamAuthType Authentication type
type UpstreamAuthType string
-// UpstreamDefinition Upstream endpoint configuration (single target or reference)
+// UpstreamDefinition Upstream endpoint configuration. Provide exactly one of `url` (a direct backend URL) or
+// `ref` (a reference to a predefined upstream definition) — never both.
type UpstreamDefinition struct {
// Auth Authentication configuration for upstream endpoints
Auth *UpstreamAuth `json:"auth,omitempty" yaml:"auth,omitempty"`
- // Ref Reference to a predefined upstreamDefinition
+ // Ref Reference to a predefined upstreamDefinition. Mutually exclusive with `url`.
Ref *string `json:"ref,omitempty" yaml:"ref,omitempty"`
- // Url Direct backend URL to route traffic to
+ // Url Direct backend URL to route traffic to. Mutually exclusive with `ref`.
Url *string `json:"url,omitempty" yaml:"url,omitempty"`
union json.RawMessage
}
@@ -2695,6 +2698,9 @@ type BadRequest = Error
// Conflict The single error shape returned by every failed request across the API.
type Conflict = Error
+// DeploymentActiveConflict The single error shape returned by every failed request across the API.
+type DeploymentActiveConflict = Error
+
// Forbidden The single error shape returned by every failed request across the API.
type Forbidden = Error
@@ -2715,7 +2721,7 @@ type Unauthorized = Error
// ListApplicationsParams defines parameters for ListApplications.
type ListApplicationsParams struct {
- // ProjectId **Project ID** consisting of the **handle** (unique slug identifier) of the Project to filter APIs by.
+ // ProjectId **Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.
ProjectId ProjectIdQ `form:"projectId" json:"projectId" yaml:"projectId"`
// Limit Maximum number of items to return per page.
@@ -2829,7 +2835,7 @@ type ListGatewayTokensParams struct {
// ListLLMProviderTemplatesParams defines parameters for ListLLMProviderTemplates.
type ListLLMProviderTemplatesParams struct {
- // Query URL-encoded search DSL. `query=latest:true` lists only the latest version of each family; `query=groupId:` lists that family's versions; adding `&version:` returns the single full template for that version. Terms are `&`-separated `key:value` pairs and the whole value is percent-encoded (e.g. groupId%3Aopenai%26version%3Av2.0).
+ // Query URL-encoded search DSL. `query=latest:true` lists only the latest version of each family; `query=groupId:` lists that family's versions; adding `&version:` returns the single full template for that version. Terms are `&`-separated `key:value` pairs and the whole value is percent-encoded (e.g. groupId%3Awso2-openai%26version%3Av2.0).
Query *string `form:"query,omitempty" json:"query,omitempty" yaml:"query,omitempty"`
// Limit Maximum number of items to return per page.
@@ -2853,6 +2859,7 @@ type CopyLLMProviderTemplateVersionParams struct {
// SetLLMProviderTemplateVersionEnabledJSONBody defines parameters for SetLLMProviderTemplateVersionEnabled.
type SetLLMProviderTemplateVersionEnabledJSONBody struct {
+ // Enabled Set to true to enable this template version, false to disable it.
Enabled bool `json:"enabled" yaml:"enabled"`
}
@@ -2876,7 +2883,7 @@ type ListLLMProviderAPIKeysParams struct {
// GetLLMProviderDeploymentsParams defines parameters for GetLLMProviderDeployments.
type GetLLMProviderDeploymentsParams struct {
- // GatewayId **Gateway ID** consisting of the **UUID** of the Gateway to filter status by.
+ // GatewayId **Gateway ID** (handle — unique slug identifier) of the Gateway to filter deployments by.
GatewayId *GatewayIdQ `form:"gatewayId,omitempty" json:"gatewayId,omitempty" yaml:"gatewayId,omitempty"`
// Status Filter deployments by status (DEPLOYED, UNDEPLOYED, DEPLOYING, UNDEPLOYING, FAILED, or ARCHIVED)
@@ -2894,13 +2901,13 @@ type GetLLMProviderDeploymentsParamsStatus string
// RestoreLLMProviderDeploymentParams defines parameters for RestoreLLMProviderDeployment.
type RestoreLLMProviderDeploymentParams struct {
- // GatewayId UUID of the gateway (validated against deployment's bound gateway)
+ // GatewayId Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)
GatewayId string `form:"gatewayId" json:"gatewayId" yaml:"gatewayId"`
}
// UndeployLLMProviderDeploymentParams defines parameters for UndeployLLMProviderDeployment.
type UndeployLLMProviderDeploymentParams struct {
- // GatewayId UUID of the gateway (validated against deployment's bound gateway)
+ // GatewayId Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)
GatewayId string `form:"gatewayId" json:"gatewayId" yaml:"gatewayId"`
}
@@ -2915,7 +2922,7 @@ type ListLLMProxiesByProviderParams struct {
// ListLLMProxiesParams defines parameters for ListLLMProxies.
type ListLLMProxiesParams struct {
- // ProjectId **Project ID** consisting of the **handle** (unique slug identifier) of the Project to filter APIs by.
+ // ProjectId **Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.
ProjectId ProjectIdQ `form:"projectId" json:"projectId" yaml:"projectId"`
// Limit Maximum number of items to return per page.
@@ -2936,7 +2943,7 @@ type ListLLMProxyAPIKeysParams struct {
// GetLLMProxyDeploymentsParams defines parameters for GetLLMProxyDeployments.
type GetLLMProxyDeploymentsParams struct {
- // GatewayId **Gateway ID** consisting of the **UUID** of the Gateway to filter status by.
+ // GatewayId **Gateway ID** (handle — unique slug identifier) of the Gateway to filter deployments by.
GatewayId *GatewayIdQ `form:"gatewayId,omitempty" json:"gatewayId,omitempty" yaml:"gatewayId,omitempty"`
// Status Filter deployments by status (DEPLOYED, UNDEPLOYED, DEPLOYING, UNDEPLOYING, FAILED, or ARCHIVED)
@@ -2954,19 +2961,19 @@ type GetLLMProxyDeploymentsParamsStatus string
// RestoreLLMProxyDeploymentParams defines parameters for RestoreLLMProxyDeployment.
type RestoreLLMProxyDeploymentParams struct {
- // GatewayId UUID of the gateway (validated against deployment's bound gateway)
+ // GatewayId Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)
GatewayId string `form:"gatewayId" json:"gatewayId" yaml:"gatewayId"`
}
// UndeployLLMProxyDeploymentParams defines parameters for UndeployLLMProxyDeployment.
type UndeployLLMProxyDeploymentParams struct {
- // GatewayId UUID of the gateway (validated against deployment's bound gateway)
+ // GatewayId Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)
GatewayId string `form:"gatewayId" json:"gatewayId" yaml:"gatewayId"`
}
// ListMCPProxiesParams defines parameters for ListMCPProxies.
type ListMCPProxiesParams struct {
- // ProjectId **Project ID** consisting of the **handle** (unique slug identifier) of the Project to filter APIs by.
+ // ProjectId **Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.
ProjectId ProjectIdQ `form:"projectId" json:"projectId" yaml:"projectId"`
// Limit Maximum number of items to return per page.
@@ -2978,7 +2985,7 @@ type ListMCPProxiesParams struct {
// GetMCPProxyDeploymentsParams defines parameters for GetMCPProxyDeployments.
type GetMCPProxyDeploymentsParams struct {
- // GatewayId **Gateway ID** consisting of the **UUID** of the Gateway to filter status by.
+ // GatewayId **Gateway ID** (handle — unique slug identifier) of the Gateway to filter deployments by.
GatewayId *GatewayIdQ `form:"gatewayId,omitempty" json:"gatewayId,omitempty" yaml:"gatewayId,omitempty"`
// Status Filter deployments by status (DEPLOYED, UNDEPLOYED, DEPLOYING, UNDEPLOYING, FAILED, or ARCHIVED)
@@ -3057,7 +3064,7 @@ type ListProjectsParamsSortOrder string
// ListRESTAPIsParams defines parameters for ListRESTAPIs.
type ListRESTAPIsParams struct {
- // ProjectId **Project ID** consisting of the **handle** (unique slug identifier) of the Project to filter APIs by.
+ // ProjectId **Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.
ProjectId ProjectIdQ `form:"projectId" json:"projectId" yaml:"projectId"`
// Limit Maximum number of items to return per page.
@@ -3084,7 +3091,7 @@ type ListRESTAPIsParamsSortOrder string
// GetDeploymentsParams defines parameters for GetDeployments.
type GetDeploymentsParams struct {
- // GatewayId **Gateway ID** consisting of the **UUID** of the Gateway to filter status by.
+ // GatewayId **Gateway ID** (handle — unique slug identifier) of the Gateway to filter deployments by.
GatewayId *GatewayIdQ `form:"gatewayId,omitempty" json:"gatewayId,omitempty" yaml:"gatewayId,omitempty"`
// Status Filter deployments by status (DEPLOYED, UNDEPLOYED, DEPLOYING, UNDEPLOYING, FAILED, or ARCHIVED)
diff --git a/platform-api/internal/handler/api.go b/platform-api/internal/handler/api.go
index 415716987c..840a0fd200 100644
--- a/platform-api/internal/handler/api.go
+++ b/platform-api/internal/handler/api.go
@@ -77,6 +77,9 @@ func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) error {
return apperror.ValidationFailed.New("At least one upstream endpoint (main or sandbox) is required").
WithLogMessage(fmt.Sprintf("no upstream endpoints provided for org %s", orgId))
}
+ if err := validateUpstreamDefinitions(req.Upstream); err != nil {
+ return err
+ }
createdBy, err := resolveActorErr(r, h.identity, "create API")
if err != nil {
@@ -171,6 +174,9 @@ func (h *APIHandler) UpdateAPI(w http.ResponseWriter, r *http.Request) error {
if isEmptyUpstreamDefinition(req.Upstream.Main) && (req.Upstream.Sandbox == nil || isEmptyUpstreamDefinition(*req.Upstream.Sandbox)) {
return apperror.ValidationFailed.New("At least one upstream endpoint (main or sandbox) is required")
}
+ if err := validateUpstreamDefinitions(req.Upstream); err != nil {
+ return err
+ }
updatedBy, err := resolveActorErr(r, h.identity, "update API")
if err != nil {
@@ -290,11 +296,25 @@ func (h *APIHandler) RegisterRoutes(mux *http.ServeMux) {
}
func isEmptyUpstreamDefinition(definition api.UpstreamDefinition) bool {
- if definition.Url != nil && *definition.Url != "" {
- return false
+ return !hasUpstreamURL(definition) && !hasUpstreamRef(definition)
+}
+
+func hasUpstreamURL(definition api.UpstreamDefinition) bool {
+ return definition.Url != nil && strings.TrimSpace(*definition.Url) != ""
+}
+
+func hasUpstreamRef(definition api.UpstreamDefinition) bool {
+ return definition.Ref != nil && strings.TrimSpace(*definition.Ref) != ""
+}
+
+// validateUpstreamDefinitions ensures every provided upstream definition specifies
+// exactly one of url or ref, as required by the UpstreamDefinition schema.
+func validateUpstreamDefinitions(upstream api.Upstream) error {
+ if hasUpstreamURL(upstream.Main) && hasUpstreamRef(upstream.Main) {
+ return apperror.ValidationFailed.New("The upstream main must specify either a url or a ref, not both.")
}
- if definition.Ref != nil && *definition.Ref != "" {
- return false
+ if upstream.Sandbox != nil && hasUpstreamURL(*upstream.Sandbox) && hasUpstreamRef(*upstream.Sandbox) {
+ return apperror.ValidationFailed.New("The upstream sandbox must specify either a url or a ref, not both.")
}
- return true
+ return nil
}
diff --git a/platform-api/internal/repository/interfaces.go b/platform-api/internal/repository/interfaces.go
index 123917b57e..5fb36bd79d 100644
--- a/platform-api/internal/repository/interfaces.go
+++ b/platform-api/internal/repository/interfaces.go
@@ -43,6 +43,7 @@ type OrganizationRepository interface {
type ProjectRepository interface {
CreateProject(project *model.Project) error
GetProjectByUUID(projectId string) (*model.Project, error)
+ GetProjectByUUIDAndOrgID(projectId, orgID string) (*model.Project, error)
GetProjectByNameAndOrgID(name, orgID string) (*model.Project, error)
GetProjectByHandleAndOrgID(handle, orgID string) (*model.Project, error)
GetProjectsByOrganizationID(orgID string) ([]*model.Project, error)
diff --git a/platform-api/internal/repository/project.go b/platform-api/internal/repository/project.go
index b99aca657b..d8e98a37fc 100644
--- a/platform-api/internal/repository/project.go
+++ b/platform-api/internal/repository/project.go
@@ -78,6 +78,32 @@ func (r *ProjectRepo) GetProjectByUUID(projectId string) (*model.Project, error)
return project, nil
}
+// GetProjectByUUIDAndOrgID retrieves a project by ID scoped to an organization.
+// Use this over GetProjectByUUID whenever the caller is serving a tenant request,
+// so a UUID belonging to another organization resolves to nothing.
+func (r *ProjectRepo) GetProjectByUUIDAndOrgID(projectId, orgID string) (*model.Project, error) {
+ project := &model.Project{}
+ query := `
+ SELECT uuid, handle, display_name, organization_uuid, description, created_by, created_at, updated_by, updated_at
+ FROM projects
+ WHERE uuid = ? AND organization_uuid = ?
+ `
+ var createdBy, updatedBy sql.NullString
+ err := r.db.QueryRow(r.db.Rebind(query), projectId, orgID).Scan(
+ &project.ID, &project.Handle, &project.Name, &project.OrganizationID, &project.Description,
+ &createdBy, &project.CreatedAt, &updatedBy, &project.UpdatedAt,
+ )
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ project.CreatedBy = createdBy.String
+ project.UpdatedBy = updatedBy.String
+ return project, nil
+}
+
// GetProjectByNameAndOrgID retrieves a project by name within an organization
func (r *ProjectRepo) GetProjectByNameAndOrgID(name, orgID string) (*model.Project, error) {
project := &model.Project{}
diff --git a/platform-api/internal/service/api.go b/platform-api/internal/service/api.go
index 3c1e5248f6..46696c3ee1 100644
--- a/platform-api/internal/service/api.go
+++ b/platform-api/internal/service/api.go
@@ -1011,10 +1011,10 @@ func (s *APIService) isEmptyUpstream(upstream api.Upstream) bool {
}
func (s *APIService) isEmptyUpstreamDefinition(definition api.UpstreamDefinition) bool {
- if definition.Url != nil && *definition.Url != "" {
+ if definition.Url != nil && strings.TrimSpace(*definition.Url) != "" {
return false
}
- if definition.Ref != nil && *definition.Ref != "" {
+ if definition.Ref != nil && strings.TrimSpace(*definition.Ref) != "" {
return false
}
return true
diff --git a/platform-api/internal/service/llm.go b/platform-api/internal/service/llm.go
index 6cc8f3b79a..eece1699a3 100644
--- a/platform-api/internal/service/llm.go
+++ b/platform-api/internal/service/llm.go
@@ -188,13 +188,54 @@ func NewLLMProxyService(
}
}
-// toProxyAPI converts m via mapProxyModelToAPI and resolves its
-// createdBy/updatedBy UUIDs to their raw external identity.
-func (s *LLMProxyService) toProxyAPI(m *model.LLMProxy) (*api.LLMProxy, error) {
+// resolveProjectHandle maps a stored project UUID to the project handle the
+// API exposes as `projectId`. Every id-shaped field in this API is a handle
+// (Create/List take a project handle), so responses must return one too —
+// clients have no way to resolve a UUID back to a project. Mirrors
+// ApplicationService.modelToApplicationResponseUnresolved.
+//
+// The lookup is scoped to orgUUID so a project UUID belonging to another
+// organization never resolves — a stored UUID is not itself proof of tenancy.
+//
+// projectHandleCache is an optional per-request memo so a listing page doesn't
+// re-query the same project once per item; pass nil for single-item responses.
+func (s *LLMProxyService) resolveProjectHandle(orgUUID, projectUUID string, projectHandleCache map[string]string) (string, error) {
+ projectUUID = strings.TrimSpace(projectUUID)
+ if projectUUID == "" {
+ return "", nil
+ }
+ if handle, ok := projectHandleCache[projectUUID]; ok {
+ return handle, nil
+ }
+ if s.projectRepo == nil {
+ return "", fmt.Errorf("cannot resolve project handle: project repository unavailable")
+ }
+ project, err := s.projectRepo.GetProjectByUUIDAndOrgID(projectUUID, orgUUID)
+ if err != nil {
+ return "", fmt.Errorf("failed to resolve project: %w", err)
+ }
+ if project == nil {
+ return "", apperror.ProjectNotFound.New()
+ }
+ if projectHandleCache != nil {
+ projectHandleCache[projectUUID] = project.Handle
+ }
+ return project.Handle, nil
+}
+
+// toProxyAPI converts m via mapProxyModelToAPI, resolves its project UUID to
+// the project handle, and resolves its createdBy/updatedBy UUIDs to their raw
+// external identity.
+func (s *LLMProxyService) toProxyAPI(orgUUID string, m *model.LLMProxy) (*api.LLMProxy, error) {
resp := mapProxyModelToAPI(m)
if resp == nil {
return nil, nil
}
+ projectHandle, err := s.resolveProjectHandle(orgUUID, resp.ProjectId, nil)
+ if err != nil {
+ return nil, err
+ }
+ resp.ProjectId = projectHandle
if err := s.identity.ResolveIdentityField(&resp.CreatedBy); err != nil {
return nil, err
}
@@ -1528,7 +1569,7 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) (
if created == nil {
return nil, apperror.LLMProxyNotFound.New()
}
- return s.toProxyAPI(created)
+ return s.toProxyAPI(orgUUID, created)
}
func (s *LLMProxyService) List(orgUUID string, projectHandle *string, limit, offset int) (*api.LLMProxyListResponse, error) {
@@ -1576,6 +1617,7 @@ func (s *LLMProxyService) List(orgUUID string, projectHandle *string, limit, off
}
resp.List = make([]api.LLMProxyListItem, 0, len(items))
createdByFields := make([]**string, 0, len(items))
+ projectHandles := make(map[string]string)
for _, p := range items {
id := p.ID
name := p.Name
@@ -1587,7 +1629,10 @@ func (s *LLMProxyService) List(orgUUID string, projectHandle *string, limit, off
contextValue = &v
}
version := p.Version
- projectID := p.ProjectUUID
+ projectID, err := s.resolveProjectHandle(orgUUID, p.ProjectUUID, projectHandles)
+ if err != nil {
+ return nil, err
+ }
provider := p.Configuration.Provider
resp.List = append(resp.List, api.LLMProxyListItem{
Id: &id,
@@ -1643,6 +1688,7 @@ func (s *LLMProxyService) ListByProvider(orgUUID, providerID string, limit, offs
}
resp.List = make([]api.LLMProxyListItem, 0, len(items))
createdByFields := make([]**string, 0, len(items))
+ projectHandles := make(map[string]string)
for _, p := range items {
id := p.ID
name := p.Name
@@ -1654,7 +1700,10 @@ func (s *LLMProxyService) ListByProvider(orgUUID, providerID string, limit, offs
contextValue = &v
}
version := p.Version
- projectID := p.ProjectUUID
+ projectID, err := s.resolveProjectHandle(orgUUID, p.ProjectUUID, projectHandles)
+ if err != nil {
+ return nil, err
+ }
provider := p.Configuration.Provider
resp.List = append(resp.List, api.LLMProxyListItem{
Id: &id,
@@ -1688,7 +1737,7 @@ func (s *LLMProxyService) Get(orgUUID, handle string) (*api.LLMProxy, error) {
if m == nil {
return nil, apperror.LLMProxyNotFound.New()
}
- return s.toProxyAPI(m)
+ return s.toProxyAPI(orgUUID, m)
}
func (s *LLMProxyService) Update(orgUUID, handle, updatedBy string, req *api.LLMProxy) (*api.LLMProxy, error) {
@@ -1833,7 +1882,7 @@ func (s *LLMProxyService) Update(orgUUID, handle, updatedBy string, req *api.LLM
return nil, apperror.LLMProxyNotFound.New()
}
_ = s.auditRepo.Record("UPDATE", existing.UUID, "llm_proxy", orgUUID, updatedBy)
- return s.toProxyAPI(updated)
+ return s.toProxyAPI(orgUUID, updated)
}
func (s *LLMProxyService) Delete(orgUUID, handle, deletedBy string) error {
@@ -1905,10 +1954,24 @@ func isSQLiteUniqueConstraint(err error) bool {
}
func validateUpstream(u api.Upstream) error {
- mainUrl := utils.ValueOrEmpty(u.Main.Url)
- mainRef := utils.ValueOrEmpty(u.Main.Ref)
- if strings.TrimSpace(mainUrl) == "" && strings.TrimSpace(mainRef) == "" {
- return apperror.ValidationFailed.New("The upstream main must specify either a url or a ref.")
+ if err := validateUpstreamDefinition("main", u.Main); err != nil {
+ return err
+ }
+ // Sandbox is optional, but when present it carries the same either-url-or-ref
+ // constraint as main.
+ if u.Sandbox != nil {
+ return validateUpstreamDefinition("sandbox", *u.Sandbox)
+ }
+ return nil
+}
+
+// validateUpstreamDefinition enforces the UpstreamDefinition schema constraint that
+// exactly one of url or ref is provided.
+func validateUpstreamDefinition(name string, definition api.UpstreamDefinition) error {
+ hasUrl := strings.TrimSpace(utils.ValueOrEmpty(definition.Url)) != ""
+ hasRef := strings.TrimSpace(utils.ValueOrEmpty(definition.Ref)) != ""
+ if hasUrl == hasRef {
+ return apperror.ValidationFailed.New(fmt.Sprintf("The upstream %s must specify either a url or a ref, not both.", name))
}
return nil
}
diff --git a/platform-api/internal/service/llm_test.go b/platform-api/internal/service/llm_test.go
index 5a5b0da2df..e28bc25b0a 100644
--- a/platform-api/internal/service/llm_test.go
+++ b/platform-api/internal/service/llm_test.go
@@ -1135,6 +1135,13 @@ func (m *mockProjectRepo) GetProjectByUUID(projectID string) (*model.Project, er
return m.project, nil
}
+func (m *mockProjectRepo) GetProjectByUUIDAndOrgID(projectID, orgID string) (*model.Project, error) {
+ if m.project != nil && m.project.OrganizationID != "" && m.project.OrganizationID != orgID {
+ return nil, nil
+ }
+ return m.project, nil
+}
+
func (m *mockProjectRepo) GetProjectByHandleAndOrgID(handle, orgID string) (*model.Project, error) {
return m.project, nil
}
@@ -1499,7 +1506,8 @@ func TestLLMProxyServiceListByProviderUsesProviderUUID(t *testing.T) {
return &model.LLMProvider{UUID: "provider-uuid", ID: providerID}, nil
},
}
- service := NewLLMProxyService(proxyRepo, providerRepo, nil, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService())
+ projectRepo := &mockProjectRepo{project: &model.Project{ID: "project-1", Handle: "test-project", OrganizationID: "org-1"}}
+ service := NewLLMProxyService(proxyRepo, providerRepo, projectRepo, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService())
resp, err := service.ListByProvider("org-1", "provider-1", 10, 0)
if err != nil {
@@ -1511,6 +1519,11 @@ func TestLLMProxyServiceListByProviderUsesProviderUUID(t *testing.T) {
if resp == nil || resp.Count != 1 || len(resp.List) != 1 {
t.Fatalf("expected one proxy in response, got: %#v", resp)
}
+ // projectId must be the project handle, not the stored UUID: clients route
+ // on handles and cannot resolve a project UUID back to one.
+ if resp.List[0].ProjectId == nil || *resp.List[0].ProjectId != "test-project" {
+ t.Fatalf("expected projectId to be the project handle, got: %v", resp.List[0].ProjectId)
+ }
}
func TestLLMProxyServiceUpdatePreservesProviderAuthValue(t *testing.T) {
@@ -1983,3 +1996,42 @@ func TestLLMProxyServiceUpdateFailsWhenAdditionalProviderNameCollides(t *testing
t.Fatalf("expected update to be rejected before persisting, but proxy was updated")
}
}
+
+// A project UUID stored on a proxy is not itself proof of tenancy — resolving
+// it must be scoped to the caller's organization, so a UUID belonging to another
+// org resolves to nothing rather than leaking that org's project handle.
+func TestLLMProxyServiceResolveProjectHandleIsOrgScoped(t *testing.T) {
+ projectRepo := &mockProjectRepo{project: &model.Project{ID: "project-1", Handle: "test-project", OrganizationID: "org-1"}}
+ service := NewLLMProxyService(nil, nil, projectRepo, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService())
+
+ handle, err := service.resolveProjectHandle("org-1", "project-1", nil)
+ if err != nil || handle != "test-project" {
+ t.Fatalf("expected same-org resolution to succeed, got handle=%q err=%v", handle, err)
+ }
+
+ if _, err := service.resolveProjectHandle("org-2", "project-1", nil); !apperror.ProjectNotFound.Is(err) {
+ t.Fatalf("expected ProjectNotFound for a cross-org project uuid, got: %v", err)
+ }
+}
+
+// The sandbox upstream is optional, but when supplied it carries the same
+// exactly-one-of-url-or-ref constraint as main — an unvalidated sandbox would let
+// an ambiguous (both) or empty (neither) endpoint through.
+func TestValidateUpstreamValidatesSandbox(t *testing.T) {
+ url := "https://api.example.com"
+ ref := "openai-default"
+ main := api.UpstreamDefinition{Url: &url}
+
+ if err := validateUpstream(api.Upstream{Main: main}); err != nil {
+ t.Fatalf("expected an absent sandbox to be accepted: %v", err)
+ }
+ if err := validateUpstream(api.Upstream{Main: main, Sandbox: &api.UpstreamDefinition{Ref: &ref}}); err != nil {
+ t.Fatalf("expected a ref-only sandbox to be accepted: %v", err)
+ }
+ if err := validateUpstream(api.Upstream{Main: main, Sandbox: &api.UpstreamDefinition{Url: &url, Ref: &ref}}); !apperror.ValidationFailed.Is(err) {
+ t.Fatalf("expected ValidationFailed for a sandbox with both url and ref, got: %v", err)
+ }
+ if err := validateUpstream(api.Upstream{Main: main, Sandbox: &api.UpstreamDefinition{}}); !apperror.ValidationFailed.Is(err) {
+ t.Fatalf("expected ValidationFailed for a sandbox with neither url nor ref, got: %v", err)
+ }
+}
diff --git a/platform-api/internal/service/mcp.go b/platform-api/internal/service/mcp.go
index f54db31eae..c4d708cbb5 100644
--- a/platform-api/internal/service/mcp.go
+++ b/platform-api/internal/service/mcp.go
@@ -24,6 +24,7 @@ import (
"errors"
"fmt"
"log/slog"
+ "strings"
"time"
"github.com/wso2/api-platform/platform-api/api"
@@ -73,13 +74,55 @@ func NewMCPProxyService(repo repository.MCPProxyRepository, projectRepo reposito
}
}
-// toMCPProxyAPI converts m via mapMCPProxyModelToAPI and resolves its
-// createdBy/updatedBy UUIDs to their raw external identity.
-func (s *MCPProxyService) toMCPProxyAPI(m *model.MCPProxy) (*api.MCPProxy, error) {
+// resolveProjectHandle maps a stored project UUID to the project handle the
+// API exposes as `projectId`. Requests identify a project by handle
+// (Create/ListByProject both take one), so responses must too — a client has
+// no way to resolve a project UUID back to a handle. Mirrors
+// LLMProxyService.resolveProjectHandle.
+//
+// The lookup is scoped to orgUUID so a project UUID belonging to another
+// organization never resolves — a stored UUID is not itself proof of tenancy.
+//
+// projectHandleCache is an optional per-request memo so a listing page doesn't
+// re-query the same project once per item; pass nil for single-item responses.
+func (s *MCPProxyService) resolveProjectHandle(orgUUID string, projectUUID *string, projectHandleCache map[string]string) (*string, error) {
+ if projectUUID == nil || strings.TrimSpace(*projectUUID) == "" {
+ return projectUUID, nil
+ }
+ uuid := strings.TrimSpace(*projectUUID)
+ if handle, ok := projectHandleCache[uuid]; ok {
+ return &handle, nil
+ }
+ if s.projectRepo == nil {
+ return nil, fmt.Errorf("cannot resolve project handle: project repository unavailable")
+ }
+ project, err := s.projectRepo.GetProjectByUUIDAndOrgID(uuid, orgUUID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve project: %w", err)
+ }
+ if project == nil {
+ return nil, apperror.ProjectNotFound.New()
+ }
+ handle := project.Handle
+ if projectHandleCache != nil {
+ projectHandleCache[uuid] = handle
+ }
+ return &handle, nil
+}
+
+// toMCPProxyAPI converts m via mapMCPProxyModelToAPI, resolves its project
+// UUID to the project handle, and resolves its createdBy/updatedBy UUIDs to
+// their raw external identity.
+func (s *MCPProxyService) toMCPProxyAPI(orgUUID string, m *model.MCPProxy) (*api.MCPProxy, error) {
resp := mapMCPProxyModelToAPI(m)
if resp == nil {
return nil, nil
}
+ projectHandle, err := s.resolveProjectHandle(orgUUID, resp.ProjectId, nil)
+ if err != nil {
+ return nil, err
+ }
+ resp.ProjectId = projectHandle
if err := s.identity.ResolveIdentityField(&resp.CreatedBy); err != nil {
return nil, err
}
@@ -231,11 +274,17 @@ func (s *MCPProxyService) List(orgUUID string, limit, offset int) (*api.MCPProxy
resp.List = make([]api.MCPProxyListItem, 0, len(proxies))
createdByFields := make([]**string, 0, len(proxies))
+ projectHandles := make(map[string]string)
for _, p := range proxies {
item := mapMCPProxyModelToListItem(p)
if item == nil {
continue
}
+ projectHandle, err := s.resolveProjectHandle(orgUUID, item.ProjectId, projectHandles)
+ if err != nil {
+ return nil, err
+ }
+ item.ProjectId = projectHandle
resp.List = append(resp.List, *item)
createdByFields = append(createdByFields, &resp.List[len(resp.List)-1].CreatedBy)
}
@@ -286,11 +335,17 @@ func (s *MCPProxyService) ListByProject(orgUUID, projectHandle string, limit, of
resp.List = make([]api.MCPProxyListItem, 0, len(proxies))
createdByFields := make([]**string, 0, len(proxies))
+ projectHandles := make(map[string]string)
for _, p := range proxies {
item := mapMCPProxyModelToListItem(p)
if item == nil {
continue
}
+ projectHandle, err := s.resolveProjectHandle(orgUUID, item.ProjectId, projectHandles)
+ if err != nil {
+ return nil, err
+ }
+ item.ProjectId = projectHandle
resp.List = append(resp.List, *item)
createdByFields = append(createdByFields, &resp.List[len(resp.List)-1].CreatedBy)
}
@@ -315,7 +370,7 @@ func (s *MCPProxyService) Get(orgUUID, handle string) (*api.MCPProxy, error) {
return nil, apperror.MCPProxyNotFound.New()
}
- return s.toMCPProxyAPI(m)
+ return s.toMCPProxyAPI(orgUUID, m)
}
// Update updates an existing MCP proxy
@@ -504,10 +559,24 @@ func (s *MCPProxyService) FetchServerInfo(orgUUID string, req *api.MCPServerInfo
var url string
var headerName, headerValue string
- if req.ProxyId != nil && *req.ProxyId != "" {
- if req.Auth != nil {
- s.slogger.Warn("Auth override is not allowed when proxyId is provided. Ignoring auth in request and using stored auth from proxy configuration.", "org_id", orgUUID, "proxy_id", *req.ProxyId)
- }
+ hasProxyID := req.ProxyId != nil && *req.ProxyId != ""
+ hasURL := req.Url != nil && *req.Url != ""
+
+ // Exactly one of url / proxyId selects the target, matching the oneOf constraint on
+ // MCPServerInfoFetchRequest. Accepting both would leave which one wins implicit.
+ if hasProxyID && hasURL {
+ return nil, apperror.ValidationFailed.New("Provide either url or proxyId, not both.")
+ }
+ if !hasProxyID && !hasURL {
+ return nil, apperror.ValidationFailed.New("Either url or proxyId is required.")
+ }
+ // Auth is only meaningful for the direct-url flow; in refetch mode the stored auth is
+ // authoritative, so a caller-supplied override is rejected rather than silently ignored.
+ if hasProxyID && req.Auth != nil {
+ return nil, apperror.ValidationFailed.New("The auth field is not allowed when proxyId is provided.")
+ }
+
+ if hasProxyID {
// ProxyId provided - fetch stored configuration (refetch flow)
// Auth override is NOT allowed in refetch - use exactly what's stored
proxy, err := s.repo.GetByHandle(*req.ProxyId, orgUUID)
@@ -547,10 +616,7 @@ func (s *MCPProxyService) FetchServerInfo(orgUUID string, req *api.MCPServerInfo
}
}
} else {
- // No proxyId - initial creation flow, url is required
- if req.Url == nil || *req.Url == "" {
- return nil, apperror.ValidationFailed.New("The url field is required when proxyId is not provided.")
- }
+ // No proxyId - initial creation flow, url selects the target
url = *req.Url
// Use provided auth (optional for initial fetch)
diff --git a/platform-api/internal/service/mcp_test.go b/platform-api/internal/service/mcp_test.go
index 01fd668ba2..bbdf6448bc 100644
--- a/platform-api/internal/service/mcp_test.go
+++ b/platform-api/internal/service/mcp_test.go
@@ -67,6 +67,26 @@ func TestMCPProxyServiceCreateRejectsInvalidPolicyVersion(t *testing.T) {
}
}
+// TestMCPProxyServiceGetReturnsProjectHandle guards the response contract:
+// projectId must be the project handle, never the internal project UUID.
+// Clients route on handles and have no way to resolve a UUID back to one.
+func TestMCPProxyServiceGetReturnsProjectHandle(t *testing.T) {
+ projectUUID := "550e8400-e29b-41d4-a716-446655440000"
+ repo := &mockMCPProxyRepository{getByHandleResult: &model.MCPProxy{
+ Handle: "mcp-proxy-1",
+ Name: "Test MCP Proxy",
+ Version: "v1.0",
+ ProjectUUID: &projectUUID,
+ }}
+ projectRepo := &mockProjectRepo{project: &model.Project{ID: projectUUID, Handle: "test-project", OrganizationID: "org-1"}}
+ service := NewMCPProxyService(repo, projectRepo, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService())
+
+ resp, err := service.Get("org-1", "mcp-proxy-1")
+ require.NoError(t, err)
+ require.NotNil(t, resp.ProjectId)
+ assert.Equal(t, "test-project", *resp.ProjectId)
+}
+
func TestMCPProxyServiceUpdateRejectsInvalidPolicyVersion(t *testing.T) {
repo := &mockMCPProxyRepository{getByHandleResult: &model.MCPProxy{Handle: "mcp-proxy-1"}}
service := NewMCPProxyService(repo, nil, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService())
@@ -140,3 +160,35 @@ func TestFetchServerInfoRefetchUsesStoredURLVerbatim(t *testing.T) {
})
}
}
+
+// TestFetchServerInfoRejectsAmbiguousTargets asserts the request-shape rules enforced by
+// FetchServerInfo, matching the oneOf constraint on MCPServerInfoFetchRequest: exactly one
+// of url/proxyId selects the target, and auth may not accompany proxyId (where the stored
+// auth is authoritative) — no branch may silently ignore a supplied field.
+func TestFetchServerInfoRejectsAmbiguousTargets(t *testing.T) {
+ url := "https://mcp.example.com/mcp"
+ proxyID := "mcp-proxy-1"
+ header, value := "X-API-Key", "secret"
+
+ tests := []struct {
+ name string
+ req *api.MCPServerInfoFetchRequest
+ }{
+ {"both url and proxyId", &api.MCPServerInfoFetchRequest{Url: &url, ProxyId: &proxyID}},
+ {"neither url nor proxyId", &api.MCPServerInfoFetchRequest{}},
+ {"auth supplied with proxyId", &api.MCPServerInfoFetchRequest{
+ ProxyId: &proxyID,
+ Auth: &api.UpstreamAuth{Header: &header, Value: &value},
+ }},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ repo := &mockMCPProxyRepository{getByHandleResult: &model.MCPProxy{Handle: proxyID}}
+ service := NewMCPProxyService(repo, nil, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService())
+
+ _, err := service.FetchServerInfo("org-1", tt.req)
+ assert.True(t, apperror.ValidationFailed.Is(err), "expected a validation failure, got: %v", err)
+ })
+ }
+}
diff --git a/platform-api/internal/utils/common.go b/platform-api/internal/utils/common.go
index 20b510e295..bd9eaa7575 100644
--- a/platform-api/internal/utils/common.go
+++ b/platform-api/internal/utils/common.go
@@ -462,25 +462,16 @@ func ValidateExternalURL(_ context.Context, rawURL string) error {
// CheckURLReachability verifies that the given URL is reachable by sending an HTTP HEAD request.
// It returns an error if the request fails or cannot complete within the given timeout.
func CheckURLReachability(rawURL string, timeout time.Duration) error {
- client := &http.Client{
- Timeout: timeout,
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
- if len(via) >= 5 {
- return errors.New("too many redirects")
- }
- return nil
- },
- // This is a one-off diagnostic probe with nothing to gain from keep-alive —
- // and everything to lose: some servers send a Transfer-Encoding: chunked
- // response to a HEAD request without properly terminating it, and Go's
- // transport can still decide the connection is safe to pool for reuse even
- // when the body is drained (see the drain below). Disabling keep-alives here
- // guarantees this connection is always closed after use, so a malformed
- // response to this probe can never leak into and corrupt a later, unrelated
- // request (e.g. the MCP initialize/tools/list calls FetchServerInfo makes
- // right after this) that happens to reuse the same pooled connection.
- Transport: &http.Transport{DisableKeepAlives: true},
- }
+ // The probed URL is user/tenant-supplied, so the connection goes through the guarded
+ // dialer: every resolved IP is checked at dial time under the upstream policy
+ // (link-local/metadata refused; private and in-cluster upstreams allowed, since that is
+ // what an MCP backend normally is) and every redirect hop is bounded and re-dialed
+ // through the same guard. The client also disables keep-alives — this is a one-off
+ // diagnostic probe with nothing to gain from connection reuse, and everything to lose:
+ // some servers answer a HEAD with an improperly terminated chunked response, and a
+ // pooled connection could then corrupt a later, unrelated request (e.g. the MCP
+ // initialize/tools/list calls FetchServerInfo makes right after this).
+ client := NewUpstreamFetchClient(timeout)
req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, rawURL, nil)
if err != nil {
diff --git a/platform-api/internal/utils/guarded_http_client.go b/platform-api/internal/utils/guarded_http_client.go
new file mode 100644
index 0000000000..74d378c3e4
--- /dev/null
+++ b/platform-api/internal/utils/guarded_http_client.go
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package utils
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ "strings"
+ "time"
+)
+
+// maxOutboundRedirects caps redirect hops on guarded clients. Every hop is still dialed
+// through the guarded dialer, so this only bounds redirect loops.
+const maxOutboundRedirects = 5
+
+// checkRedirectPolicy builds the CheckRedirect callback shared by every guarded client. It
+// bounds redirect loops, keeps the scheme within http/https, and refuses any hop that leaves
+// the host of the original request.
+//
+// The host check is what stops a credential leak: callers pass their own headers on these
+// requests (an MCP endpoint's auth header, for instance), and net/http only strips
+// Authorization/Cookie-style headers across hosts — a custom header name is forwarded
+// verbatim. A malicious or compromised upstream could otherwise answer with a redirect to a
+// host it controls and be handed the caller's credential. Same-host redirects are still
+// dialed through the guarded dialer, so the address policy continues to apply per hop.
+func checkRedirectPolicy(maxRedirects int) func(*http.Request, []*http.Request) error {
+ return func(req *http.Request, via []*http.Request) error {
+ if len(via) >= maxRedirects {
+ return fmt.Errorf("too many redirects")
+ }
+ if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
+ return fmt.Errorf("redirect to a disallowed scheme")
+ }
+ // via[0] is the original request; Host carries the port, so a port change counts
+ // as a different host too.
+ if len(via) > 0 && !strings.EqualFold(req.URL.Host, via[0].URL.Host) {
+ return fmt.Errorf("redirect to a different host")
+ }
+ return nil
+ }
+}
+
+// guardedDialContext builds a DialContext that resolves the target host itself, refuses to
+// connect when any resolved address fails the supplied policy, and then dials the exact IP
+// it just approved. Doing the resolution and the connection in one step is what closes the
+// DNS-rebinding window: a name that answers with an allowed address during a pre-check
+// cannot answer with a different one at connect time.
+//
+// The address policy is a parameter because different call sites have genuinely different
+// threat models — see isPublicIP (fetching from the public internet) versus
+// isAllowedUpstreamIP (reaching a backend the operator deployed).
+func guardedDialContext(allow func(net.IP) bool, timeout time.Duration) func(context.Context, string, string) (net.Conn, error) {
+ return func(ctx context.Context, network, addr string) (net.Conn, error) {
+ host, port, err := net.SplitHostPort(addr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid address")
+ }
+
+ ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve host")
+ }
+ if len(ips) == 0 {
+ return nil, fmt.Errorf("host has no addresses")
+ }
+ for _, ip := range ips {
+ if !allow(ip.IP) {
+ return nil, fmt.Errorf("host resolves to a disallowed address")
+ }
+ }
+
+ dialer := &net.Dialer{Timeout: timeout}
+ var lastErr error
+ for _, ip := range ips {
+ conn, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(ip.IP.String(), port))
+ if dialErr == nil {
+ return conn, nil
+ }
+ lastErr = dialErr
+ }
+ if lastErr != nil {
+ return nil, fmt.Errorf("failed to connect to host")
+ }
+ return nil, fmt.Errorf("failed to connect to host")
+ }
+}
+
+// isAllowedUpstreamIP is the address policy for calls to a backend the operator configured
+// — an MCP proxy upstream, for instance. Such a backend is normally *meant* to be private:
+// a Kubernetes ClusterIP, a service-DNS name resolving into RFC 1918 space, or a localhost
+// port during development. Refusing those would break the ordinary deployment shape, so
+// private, ULA, shared-address-space and loopback addresses are all permitted.
+//
+// What stays refused is the set of addresses that is never a legitimate upstream but is a
+// standard SSRF target or a hazard:
+//
+// - link-local unicast — 169.254.0.0/16 and fe80::/10, which is where the cloud instance
+// metadata endpoint (169.254.169.254) lives
+// - the unspecified address (0.0.0.0, ::), which the OS reinterprets as "local host"
+// - multicast and the IPv4 broadcast address, which are not meaningful HTTP peers
+func isAllowedUpstreamIP(ip net.IP) bool {
+ if ip == nil {
+ return false
+ }
+ if ip.IsLinkLocalUnicast() ||
+ ip.IsLinkLocalMulticast() ||
+ ip.IsMulticast() ||
+ ip.IsUnspecified() ||
+ ip.Equal(net.IPv4bcast) {
+ return false
+ }
+ return true
+}
+
+// upstreamIPIsAllowed is the address check used by NewUpstreamFetchClient. It is a package
+// variable only so tests can vary the policy; production code never reassigns it.
+var upstreamIPIsAllowed = isAllowedUpstreamIP
+
+// NewUpstreamFetchClient returns an http.Client for calls to an operator- or
+// tenant-configured backend whose URL is not fully trusted (MCP reachability probes, MCP
+// JSON-RPC calls). Every connection — including each redirect hop — is dialed through the
+// guarded dialer under isAllowedUpstreamIP, so private and in-cluster upstreams work
+// normally while link-local/metadata addresses stay unreachable.
+//
+// For fetching from the public internet, use the stricter isPublicIP-based path in
+// FetchOpenAPISpecFromURL instead.
+//
+// A non-positive timeout falls back to defaultUpstreamFetchTimeout.
+func NewUpstreamFetchClient(timeout time.Duration) *http.Client {
+ if timeout <= 0 {
+ timeout = defaultUpstreamFetchTimeout
+ }
+ return &http.Client{
+ Timeout: timeout,
+ Transport: &http.Transport{
+ DialContext: guardedDialContext(func(ip net.IP) bool {
+ return upstreamIPIsAllowed(ip)
+ }, timeout),
+ TLSHandshakeTimeout: timeout,
+ ResponseHeaderTimeout: timeout,
+ // A one-off outbound probe/call has nothing to gain from connection reuse, and
+ // pooling a connection whose response was malformed can corrupt a later request.
+ DisableKeepAlives: true,
+ // No proxy: dialing must go through the guarded dialer, not a forward proxy
+ // that could bypass the address checks.
+ Proxy: nil,
+ },
+ CheckRedirect: checkRedirectPolicy(maxOutboundRedirects),
+ }
+}
+
+// defaultUpstreamFetchTimeout bounds a guarded upstream call end to end when the caller
+// does not supply its own timeout.
+const defaultUpstreamFetchTimeout = 10 * time.Second
diff --git a/platform-api/internal/utils/guarded_http_client_test.go b/platform-api/internal/utils/guarded_http_client_test.go
new file mode 100644
index 0000000000..ffd2074376
--- /dev/null
+++ b/platform-api/internal/utils/guarded_http_client_test.go
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package utils
+
+import (
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+// TestIsAllowedUpstreamIP pins the upstream address policy in both directions. An MCP
+// upstream is normally a service the operator deployed, so private/in-cluster/loopback
+// addresses MUST stay reachable — a regression to a public-only policy would break every
+// cluster-internal MCP proxy. The metadata endpoint must stay unreachable regardless.
+func TestIsAllowedUpstreamIP(t *testing.T) {
+ allowed := []string{
+ "10.4.2.9", // k8s ClusterIP / RFC 1918
+ "172.16.0.5", // RFC 1918
+ "192.168.1.20", // RFC 1918
+ "100.64.0.1", // RFC 6598 shared address space
+ "127.0.0.1", // loopback — local development
+ "::1", // IPv6 loopback
+ "fd00::1", // IPv6 ULA — in-cluster
+ "93.184.216.34", // public
+ "2606:2800:220::", // public IPv6
+ }
+ for _, s := range allowed {
+ if ip := net.ParseIP(s); !isAllowedUpstreamIP(ip) {
+ t.Errorf("expected %s to be an allowed upstream address", s)
+ }
+ }
+
+ denied := []string{
+ "169.254.169.254", // cloud instance metadata
+ "169.254.1.1", // link-local
+ "fe80::1", // IPv6 link-local
+ "0.0.0.0", // unspecified
+ "::", // IPv6 unspecified
+ "224.0.0.1", // multicast
+ "255.255.255.255", // broadcast
+ }
+ for _, s := range denied {
+ if ip := net.ParseIP(s); isAllowedUpstreamIP(ip) {
+ t.Errorf("expected %s to be a denied upstream address", s)
+ }
+ }
+
+ if isAllowedUpstreamIP(nil) {
+ t.Error("expected a nil address to be denied")
+ }
+}
+
+// TestUpstreamFetchClientReachesLoopbackBackend is the end-to-end counterpart: the client
+// must actually connect to a private/loopback backend, not merely rate the address as
+// allowed. This is the regression guard for MCP proxies with in-cluster upstreams.
+func TestUpstreamFetchClientReachesLoopbackBackend(t *testing.T) {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer srv.Close()
+
+ resp, err := NewUpstreamFetchClient(0).Get(srv.URL)
+ if err != nil {
+ t.Fatalf("guarded upstream client failed to reach a loopback backend: %v", err)
+ }
+ defer resp.Body.Close() //nolint:errcheck
+ if resp.StatusCode != http.StatusOK {
+ t.Errorf("expected 200 from the test backend, got %d", resp.StatusCode)
+ }
+}
+
+// TestUpstreamFetchClientRefusesDeniedAddress confirms the dialer refuses a denied address
+// even though the policy is otherwise permissive.
+func TestUpstreamFetchClientRefusesDeniedAddress(t *testing.T) {
+ restore := upstreamIPIsAllowed
+ upstreamIPIsAllowed = func(net.IP) bool { return false }
+ defer func() { upstreamIPIsAllowed = restore }()
+
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+ defer srv.Close()
+
+ if _, err := NewUpstreamFetchClient(0).Get(srv.URL); err == nil {
+ t.Fatal("expected the guarded client to refuse a disallowed address")
+ }
+}
+
+// TestUpstreamFetchClientRefusesCrossHostRedirect guards the credential-leak case: callers
+// pass a custom auth header on MCP calls, and net/http forwards a custom header name across
+// hosts, so a redirect off the original host must not be followed. A same-host redirect
+// still is.
+func TestUpstreamFetchClientRefusesCrossHostRedirect(t *testing.T) {
+ target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer target.Close()
+
+ crossHost := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, target.URL+"/stolen", http.StatusFound)
+ }))
+ defer crossHost.Close()
+
+ if _, err := NewUpstreamFetchClient(0).Get(crossHost.URL); err == nil {
+ t.Fatal("expected the guarded client to refuse a cross-host redirect")
+ }
+
+ sameHost := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/moved" {
+ http.Redirect(w, r, "/final", http.StatusFound)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer sameHost.Close()
+
+ resp, err := NewUpstreamFetchClient(0).Get(sameHost.URL + "/moved")
+ if err != nil {
+ t.Fatalf("expected a same-host redirect to be followed: %v", err)
+ }
+ defer resp.Body.Close() //nolint:errcheck
+ if resp.StatusCode != http.StatusOK {
+ t.Errorf("expected 200 after a same-host redirect, got %d", resp.StatusCode)
+ }
+}
diff --git a/platform-api/internal/utils/mcp.go b/platform-api/internal/utils/mcp.go
index ad2da1f96b..d46f6f8518 100644
--- a/platform-api/internal/utils/mcp.go
+++ b/platform-api/internal/utils/mcp.go
@@ -51,6 +51,9 @@ const (
McpSessionHeader = "mcp-session-id"
)
+// mcpRequestTimeout bounds each outbound MCP JSON-RPC call (DNS + connect + TLS + read).
+const mcpRequestTimeout = 10 * time.Second
+
// JsonRPCRequest represents a JSON-RPC request
type JsonRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
@@ -276,9 +279,9 @@ func initializeMCPServer(url string, headerName string, headerValue string) (str
httpReq.Header.Set(headerName, headerValue)
}
}
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
+ // The MCP endpoint URL is user/tenant-supplied — dial through the SSRF-guarded client
+ // so scheme, resolved-IP and redirect restrictions apply to this call too.
+ client := NewUpstreamFetchClient(mcpRequestTimeout)
resp, err := client.Do(httpReq)
if err != nil {
return "", nil, fmt.Errorf("failed to reach MCP server for initialize: %w", err)
@@ -356,9 +359,9 @@ func postJSONRPCWithSession(url string, req any, sessionID string, headerName st
if sessionID != "" {
httpReq.Header.Set(McpSessionHeader, sessionID)
}
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
+ // Same SSRF-guarded client as the initialize call: the session/tools/prompts/resources
+ // requests target the same user-supplied URL and must be dialed under the same guard.
+ client := NewUpstreamFetchClient(mcpRequestTimeout)
resp, err := client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
diff --git a/platform-api/internal/utils/openapi_spec_fetcher.go b/platform-api/internal/utils/openapi_spec_fetcher.go
index abd7cb6301..82d05bc61b 100644
--- a/platform-api/internal/utils/openapi_spec_fetcher.go
+++ b/platform-api/internal/utils/openapi_spec_fetcher.go
@@ -51,7 +51,8 @@ const (
// DNS-rebinding) — loopback, private (RFC 1918 / ULA), link-local (incl. the cloud
// metadata endpoint 169.254.169.254), unspecified, multicast and broadcast addresses
// are refused.
-// - Redirects are bounded and each hop is dialed through the same guarded dialer.
+// - Redirects are bounded, may not leave the original host, and each hop is dialed
+// through the same guarded dialer.
// - The response body is read through an io.LimitReader capped at maxBytes.
// - Errors are returned sterile (no internal host/IP detail) so callers can log them
// internally without leaking infrastructure information to clients.
@@ -87,15 +88,7 @@ func FetchOpenAPISpecFromURL(ctx context.Context, rawURL string, maxBytes int64)
// that could bypass the IP checks.
Proxy: nil,
},
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
- if len(via) >= openAPISpecMaxRedirects {
- return fmt.Errorf("too many redirects")
- }
- if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
- return fmt.Errorf("redirect to a disallowed scheme")
- }
- return nil
- },
+ CheckRedirect: checkRedirectPolicy(openAPISpecMaxRedirects),
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
@@ -134,41 +127,12 @@ func FetchOpenAPISpecFromURL(ctx context.Context, rawURL string, maxBytes int64)
var ipIsAllowed = isPublicIP
// ssrfSafeDialContext resolves the target host and refuses to connect to any non-public
-// address. It performs the resolution itself and dials the resolved IP directly, so a
-// DNS name that resolves to a public IP during a pre-check cannot be re-resolved to an
-// internal IP at connect time (DNS rebinding).
+// address — the right policy here because an OpenAPI spec URL points at a vendor endpoint
+// on the public internet. A backend the operator deployed is a different case and uses the
+// more permissive isAllowedUpstreamIP policy instead (see NewUpstreamFetchClient).
func ssrfSafeDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
- host, port, err := net.SplitHostPort(addr)
- if err != nil {
- return nil, fmt.Errorf("invalid address")
- }
-
- ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
- if err != nil {
- return nil, fmt.Errorf("failed to resolve host")
- }
- if len(ips) == 0 {
- return nil, fmt.Errorf("host has no addresses")
- }
- for _, ip := range ips {
- if !ipIsAllowed(ip.IP) {
- return nil, fmt.Errorf("host resolves to a disallowed address")
- }
- }
-
- dialer := &net.Dialer{Timeout: openAPISpecFetchTimeout}
- var lastErr error
- for _, ip := range ips {
- conn, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(ip.IP.String(), port))
- if dialErr == nil {
- return conn, nil
- }
- lastErr = dialErr
- }
- if lastErr != nil {
- return nil, fmt.Errorf("failed to connect to host")
- }
- return nil, fmt.Errorf("failed to connect to host")
+ dial := guardedDialContext(func(ip net.IP) bool { return ipIsAllowed(ip) }, openAPISpecFetchTimeout)
+ return dial(ctx, network, addr)
}
// cgnatRange is RFC 6598 shared address space (carrier-grade NAT): 100.64.0.0/10.
diff --git a/platform-api/resources/openapi.yaml b/platform-api/resources/openapi.yaml
index e0e3ec713c..33b2af173f 100644
--- a/platform-api/resources/openapi.yaml
+++ b/platform-api/resources/openapi.yaml
@@ -242,9 +242,9 @@ paths:
/projects/{projectId}:
get:
- summary: Get project by UUID
+ summary: Get project by ID
description: |
- Retrieves a specific project by its UUID. Access is validated against the organization
+ Retrieves a specific project by its ID (handle). Access is validated against the organization
in the JWT token to ensure users can only access projects in their organization.
operationId: GetProject
security:
@@ -314,8 +314,8 @@ paths:
delete:
summary: Delete project
description: |
- Deletes a specific project by its UUID. Access is validated against the organization
- in the JWT token.
+ Deletes a specific project by its handle (unique slug identifier). Access is validated
+ against the organization in the JWT token.
operationId: DeleteProject
security:
- OAuth2Security:
@@ -420,9 +420,9 @@ paths:
/rest-apis/{restApiId}:
get:
- summary: Get REST API by UUID
+ summary: Get REST API by ID
description: |
- Retrieves a specific API by its UUID. Access is validated against the organization
+ Retrieves a specific API by its ID (handle). Access is validated against the organization
in the JWT token.
operationId: GetRESTAPI
security:
@@ -493,7 +493,7 @@ paths:
delete:
summary: Delete REST API
description: |
- Deletes a specific API by its UUID. Access is validated against the organization
+ Deletes a specific API by its ID (handle). Access is validated against the organization
in the JWT token.
operationId: DeleteRESTAPI
security:
@@ -780,6 +780,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentDeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -795,7 +798,7 @@ paths:
summary: Get deployments for a REST API
description: |
Retrieves all deployment artifacts for a specific API. The apiId parameter is the API handle (identifier),
- not the UUID. Supports filtering by gateway UUID and deployment status.
+ not the UUID. Supports filtering by gateway handle and deployment status.
Access is validated against the organization in the JWT token.
operationId: GetDeployments
security:
@@ -888,7 +891,7 @@ paths:
'404':
$ref: '#/components/responses/NotFound'
'409':
- $ref: '#/components/responses/Conflict'
+ $ref: '#/components/responses/DeploymentActiveConflict'
'500':
$ref: '#/components/responses/InternalServerError'
@@ -935,6 +938,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentUndeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -991,6 +997,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentDeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -1179,6 +1188,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/LLMProviderTemplate'
+ examples:
+ default:
+ $ref: '#/components/examples/CopiedTemplateVersion'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -1302,7 +1314,8 @@ paths:
properties:
enabled:
type: boolean
- example: false
+ description: Set to true to enable this template version, false to disable it.
+ example: true
responses:
'200':
description: Updated template version
@@ -1620,6 +1633,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentDeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -1635,7 +1651,7 @@ paths:
summary: Get deployments for an LLM provider
description: |
Retrieves all deployment artifacts for a specific LLM provider. The providerId parameter is the
- LLM provider handle (identifier), not the UUID. Supports filtering by gateway UUID and deployment status.
+ LLM provider handle (identifier), not the UUID. Supports filtering by gateway handle and deployment status.
Access is validated against the organization in the JWT token.
operationId: getLLMProviderDeployments
security:
@@ -1743,7 +1759,7 @@ paths:
'404':
$ref: '#/components/responses/NotFound'
'409':
- $ref: '#/components/responses/Conflict'
+ $ref: '#/components/responses/DeploymentActiveConflict'
'500':
$ref: '#/components/responses/InternalServerError'
@@ -1784,7 +1800,10 @@ paths:
required: true
schema:
type: string
- description: UUID of the gateway (validated against deployment's bound gateway)
+ pattern: '^[a-z0-9-]+$'
+ minLength: 3
+ maxLength: 63
+ description: Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)
responses:
'200':
description: Undeploy initiated successfully. Returns the deployment with initial status UNDEPLOYING. Poll status for final result.
@@ -1792,6 +1811,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentUndeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -1842,7 +1864,10 @@ paths:
required: true
schema:
type: string
- description: UUID of the gateway (validated against deployment's bound gateway)
+ pattern: '^[a-z0-9-]+$'
+ minLength: 3
+ maxLength: 63
+ description: Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)
responses:
'200':
description: Restore initiated successfully. Returns the deployment with initial status DEPLOYING. Poll status for final result.
@@ -1850,6 +1875,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentDeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -2243,6 +2271,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentDeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -2258,7 +2289,7 @@ paths:
summary: Get deployments for an LLM proxy
description: |
Retrieves all deployment artifacts for a specific LLM proxy. The proxyId parameter is the
- LLM proxy handle (identifier), not the UUID. Supports filtering by gateway UUID and deployment status.
+ LLM proxy handle (identifier), not the UUID. Supports filtering by gateway handle and deployment status.
Access is validated against the organization in the JWT token.
operationId: getLLMProxyDeployments
security:
@@ -2366,7 +2397,7 @@ paths:
'404':
$ref: '#/components/responses/NotFound'
'409':
- $ref: '#/components/responses/Conflict'
+ $ref: '#/components/responses/DeploymentActiveConflict'
'500':
$ref: '#/components/responses/InternalServerError'
@@ -2407,7 +2438,10 @@ paths:
required: true
schema:
type: string
- description: UUID of the gateway (validated against deployment's bound gateway)
+ pattern: '^[a-z0-9-]+$'
+ minLength: 3
+ maxLength: 63
+ description: Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)
responses:
'200':
description: Undeploy initiated successfully. Returns the deployment with initial status UNDEPLOYING. Poll status for final result.
@@ -2415,6 +2449,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentUndeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -2465,7 +2502,10 @@ paths:
required: true
schema:
type: string
- description: UUID of the gateway (validated against deployment's bound gateway)
+ pattern: '^[a-z0-9-]+$'
+ minLength: 3
+ maxLength: 63
+ description: Handle (URL-friendly slug) of the gateway (validated against deployment's bound gateway)
responses:
'200':
description: Restore initiated successfully. Returns the deployment with initial status DEPLOYING. Poll status for final result.
@@ -2473,6 +2513,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentDeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -2826,6 +2869,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentDeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -2839,7 +2885,7 @@ paths:
summary: Get deployments for an MCP proxy
description: |
Retrieves all deployment artifacts for a specific MCP proxy. The id parameter is the MCP proxy handle (identifier),
- not the UUID. Supports filtering by gateway UUID and deployment status.
+ not the UUID. Supports filtering by gateway handle and deployment status.
Access is validated against the organization in the JWT token.
operationId: GetMCPProxyDeployments
security:
@@ -2945,7 +2991,7 @@ paths:
'404':
$ref: '#/components/responses/NotFound'
'409':
- $ref: '#/components/responses/Conflict'
+ $ref: '#/components/responses/DeploymentActiveConflict'
'500':
$ref: '#/components/responses/InternalServerError'
@@ -2997,6 +3043,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentUndeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -3056,6 +3105,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeploymentResponse'
+ examples:
+ default:
+ $ref: '#/components/examples/DeploymentDeploying'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -3081,7 +3133,7 @@ paths:
tags:
- MCP Proxies
requestBody:
- description: Deployment request with gateway ID, base reference, and metadata
+ description: Target MCP server to introspect — either a direct `url` (with optional `auth`), or a `proxyId` to refetch using a stored proxy configuration.
required: true
content:
application/json:
@@ -3180,9 +3232,9 @@ paths:
/gateways/{gatewayId}:
get:
- summary: Get gateway by UUID
+ summary: Get gateway by ID
description: |
- Retrieves a specific gateway by its UUID. Access is validated against the organization
+ Retrieves a specific gateway by its ID (handle). Access is validated against the organization
in the JWT token.
operationId: GetGateway
security:
@@ -4279,8 +4331,7 @@ paths:
get:
summary: Get subscription by ID
description: |
- Returns a single subscription by ID (scoped to organization).
- Query parameter `subscriberId` is required and must match the subscription's subscriber for access control.
+ Returns a single subscription by ID, scoped to the organization in the access token.
operationId: GetSubscription
security:
- OAuth2Security:
@@ -5385,7 +5436,7 @@ components:
maxLength: 232
minLength: 1
type: string
- example: pizza
+ example: /pizza
version:
maxLength: 30
minLength: 1
@@ -6084,8 +6135,8 @@ components:
example: "def45678-g901-23hi-j456-789012klmnop"
token:
type: string
- description: Plain-text new authentication token (only exposed once during rotation)
- example: "nM8pQ1rT4vW7yZ0bC3eF6hI9kL2nO5qR8tU1wX4zA7cE0fH3jK6mP9sV2yB5dG8i"
+ description: Plain-text new authentication token (only exposed once during rotation). The example value is a non-functional placeholder.
+ example: "REDACTED_TOKEN"
createdAt:
type: string
format: date-time
@@ -6591,7 +6642,7 @@ components:
field:
type: string
description: Path of the offending field.
- example: spec.context
+ example:
message:
type: string
description: Why the field failed validation.
@@ -6722,16 +6773,25 @@ components:
oneOf:
- required: [ "url" ]
- required: [ "ref" ]
- description: Upstream endpoint configuration (single target or reference)
+ description: |
+ Upstream endpoint configuration. Provide exactly one of `url` (a direct backend URL) or
+ `ref` (a reference to a predefined upstream definition) — never both.
+ example:
+ url: http://prod-backend:5000/api/v2
+ auth:
+ type: api-key
+ header: X-API-Key
+ value: my-api-key-value
properties:
url:
type: string
format: uri
- description: Direct backend URL to route traffic to
+ description: Direct backend URL to route traffic to. Mutually exclusive with `ref`.
example: http://prod-backend:5000/api/v2
ref:
type: string
- description: Reference to a predefined upstreamDefinition
+ description: Reference to a predefined upstreamDefinition. Mutually exclusive with `url`.
+ example: my-shared-upstream
auth:
$ref: '#/components/schemas/UpstreamAuth'
@@ -6823,17 +6883,41 @@ components:
description: Resource path pattern this mapping applies to (for example /responses or /chat/*)
example: /responses
promptTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.usage.inputTokens"
completionTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.usage.outputTokens"
totalTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.usage.totalTokens"
remainingTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: header
+ identifier: "x-ratelimit-remaining-tokens"
requestModel:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.model"
responseModel:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.model"
LLMProviderTemplateResourceMappings:
type: object
@@ -6933,17 +7017,41 @@ components:
metadata:
$ref: '#/components/schemas/LLMProviderTemplateMetadata'
promptTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.usage.inputTokens"
completionTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.usage.outputTokens"
totalTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.usage.totalTokens"
remainingTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: header
+ identifier: "x-ratelimit-remaining-tokens"
requestModel:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.model"
responseModel:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.model"
resourceMappings:
$ref: '#/components/schemas/LLMProviderTemplateResourceMappings'
createdAt:
@@ -7000,17 +7108,41 @@ components:
metadata:
$ref: '#/components/schemas/LLMProviderTemplateMetadata'
promptTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.usage.inputTokens"
completionTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.usage.outputTokens"
totalTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.usage.totalTokens"
remainingTokens:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: header
+ identifier: "x-ratelimit-remaining-tokens"
requestModel:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.model"
responseModel:
- $ref: '#/components/schemas/ExtractionIdentifier'
+ allOf:
+ - $ref: '#/components/schemas/ExtractionIdentifier'
+ example:
+ location: payload
+ identifier: "$.model"
resourceMappings:
$ref: '#/components/schemas/LLMProviderTemplateResourceMappings'
@@ -7449,7 +7581,7 @@ components:
example: v1.0
context:
type: string
- description: Base path for all REST API routes (must start with /, no trailing slash)
+ description: Base path for all routes exposed by this provider. Must start with / and carry no trailing slash; the single exception is the root path "/", which is the default.
pattern: '^\/([a-zA-Z0-9_\-.\/]*[^\/])?$'
default: /
minLength: 1
@@ -7464,7 +7596,7 @@ components:
pattern: '^(\*\.|[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
minLength: 1
maxLength: 253
- example: "api.openai"
+ example: "api.openai.com"
template:
type: string
description: Template name to use for this LLM Provider
@@ -7719,11 +7851,14 @@ components:
example: v1.0
projectId:
type: string
- description: UUID of the project this proxy belongs to
- example: 550e8400-e29b-41d4-a716-446655440000
+ pattern: '^[a-z0-9-]+$'
+ minLength: 3
+ maxLength: 63
+ description: Handle (URL-friendly slug) of the project this proxy belongs to
+ example: default-project
context:
type: string
- description: Base path for all REST API routes (must start with /, no trailing slash)
+ description: Base path for all routes exposed by this proxy. Must start with / and carry no trailing slash; the single exception is the root path "/", which is the default.
pattern: '^\/([a-zA-Z0-9_\-.\/]*[^\/])?$'
default: /
minLength: 1
@@ -7738,7 +7873,7 @@ components:
pattern: '^(\*\.|[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
minLength: 1
maxLength: 253
- example: "api.openai"
+ example: "api.openai.com"
provider:
$ref: '#/components/schemas/LLMProxyProvider'
additionalProviders:
@@ -7834,8 +7969,11 @@ components:
example: v1.0
projectId:
type: string
- description: UUID of the project this proxy belongs to
- example: 550e8400-e29b-41d4-a716-446655440000
+ pattern: '^[a-z0-9-]+$'
+ minLength: 3
+ maxLength: 63
+ description: Handle (URL-friendly slug) of the project this proxy belongs to
+ example: default-project
provider:
type: string
description: Unique id of a deployed llm provider
@@ -8001,8 +8139,8 @@ components:
example: "production-key"
apiKey:
type: string
- description: The generated API key value (shown only once, 64 hexadecimal characters)
- example: "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
+ description: The generated API key value — 64 hexadecimal characters, returned only in this creation response and never retrievable afterwards. The example value is a non-functional placeholder.
+ example: "REDACTED_API_KEY"
CreateLLMProxyAPIKeyRequest:
type: object
required:
@@ -8060,8 +8198,8 @@ components:
example: "production-key"
apiKey:
type: string
- description: The generated API key value (shown only once, 64 hexadecimal characters)
- example: "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
+ description: The generated API key value — 64 hexadecimal characters, returned only in this creation response and never retrievable afterwards. The example value is a non-functional placeholder.
+ example: "REDACTED_API_KEY"
MCPProxy:
type: object
@@ -8110,11 +8248,14 @@ components:
example: v1.0
projectId:
type: string
- description: UUID of the project this proxy belongs to
- example: 550e8400-e29b-41d4-a716-446655440000
+ pattern: '^[a-z0-9-]+$'
+ minLength: 3
+ maxLength: 63
+ description: Handle (URL-friendly slug) of the project this proxy belongs to
+ example: default-project
context:
type: string
- description: Base path for all REST API routes (must start with /, no trailing slash)
+ description: Base path for all routes exposed by this proxy. Must start with / and carry no trailing slash; the single exception is the root path "/", which is the default.
pattern: '^\/([a-zA-Z0-9_\-.\/]*[^\/])?$'
default: /
minLength: 1
@@ -8208,8 +8349,11 @@ components:
example: v1.0
projectId:
type: string
- description: UUID of the project this proxy belongs to
- example: 550e8400-e29b-41d4-a716-446655440000
+ pattern: '^[a-z0-9-]+$'
+ minLength: 3
+ maxLength: 63
+ description: Handle (URL-friendly slug) of the project this proxy belongs to
+ example: default-project
status:
type: string
enum:
@@ -8252,27 +8396,37 @@ components:
MCPServerInfoFetchRequest:
type: object
+ oneOf:
+ - required: [ "url" ]
+ - required: [ "proxyId" ]
+ not:
+ required: [ "auth" ]
+ description: |
+ Target MCP server to introspect. Provide exactly one of `url` (a direct backend URL,
+ optionally with `auth`) or `proxyId` (refetch using a stored proxy configuration) — never
+ both. `auth` must not be sent together with `proxyId`; the stored auth is always used in
+ the refetch flow.
properties:
url:
type: string
format: uri
description: |
- Endpoint URL of the MCP server to fetch information from.
- Required when proxyId is not provided. When proxyId is provided,
- the URL from the stored proxy configuration is used.
+ Endpoint URL of the MCP server to fetch information from. Mutually exclusive
+ with `proxyId`; exactly one of the two must be provided.
example: https://mcp.server.com/mcp
proxyId:
type: string
description: |
- MCP proxy handle (identifier) for refresh operations. When provided,
- the server fetches URL and auth from the stored proxy configuration.
- Auth override is not allowed in refetch mode.
+ MCP proxy handle (identifier) for refresh operations. When provided, the server
+ fetches URL and auth from the stored proxy configuration. Mutually exclusive
+ with `url`; exactly one of the two must be provided.
example: my-mcp-proxy
auth:
$ref: '#/components/schemas/UpstreamAuth'
description: |
- Authentication configuration for the fetch request.
- Only allowed when proxyId is not provided (initial creation flow).
+ Authentication configuration for the fetch request. Allowed only alongside
+ `url` (initial creation flow); sending it with `proxyId` is rejected, as the
+ stored auth is used in the refetch flow.
MCPServerInfoFetchResponse:
type: object
@@ -8335,6 +8489,7 @@ components:
value:
type: string
description: Plaintext secret value — encrypted at rest, never returned in any response
+ writeOnly: true
example: sk-xxx
type:
type: string
@@ -8363,6 +8518,7 @@ components:
value:
type: string
description: New plaintext secret value — re-encrypted at rest
+ writeOnly: true
SecretResponse:
type: object
@@ -8517,8 +8673,8 @@ components:
code: VALIDATION_FAILED
message: The request failed validation.
errors:
- - field: spec.context
- message: must start with /
+ - field:
+ message:
Forbidden:
description: Forbidden. The authenticated user does not have permission to access this resource.
content:
@@ -8540,7 +8696,7 @@ components:
code: NOT_FOUND
message: The specified resource does not exist.
Conflict:
- description: Conflict. Specified resource already exists.
+ description: Conflict. The request conflicts with the current state of the resource.
content:
application/json:
schema:
@@ -8548,7 +8704,17 @@ components:
example:
status: error
code: CONFLICT
- message: The specified resource already exists.
+ message: The request conflicts with the current state of the resource.
+ DeploymentActiveConflict:
+ description: Conflict. The deployment is still active and must be undeployed before deletion.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ status: error
+ code: DEPLOYMENT_ACTIVE
+ message: Cannot delete an active deployment - undeploy it first.
InternalServerError:
description: Internal Server Error.
content:
@@ -8585,6 +8751,44 @@ components:
trackingId: 4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11
examples:
+ CopiedTemplateVersion:
+ summary: New version created by copying openai-v3-0
+ value:
+ id: openai-v4-0
+ groupId: openai
+ displayName: OpenAI
+ managedBy: organization
+ description: Default OpenAI template
+ createdBy: john.doe
+ updatedBy: john.doe
+ readOnly: false
+ version: v4.0
+ isLatest: true
+ enabled: true
+ DeploymentDeploying:
+ summary: Deployment initiated (transitional)
+ description: Asynchronous operation accepted; poll the deployment until status becomes DEPLOYED or FAILED.
+ value:
+ deploymentId: a73c85a1-d857-491e-a6b2-51dce05de7a2
+ name: v1.0-production
+ gatewayId: prod-gateway-01
+ status: DEPLOYING
+ baseDeploymentId: be6d8692-b9de-400e-b6c1-14db50154e27
+ metadata: {}
+ createdAt: 2019-08-24T14:15:22Z
+ updatedAt: 2019-08-24T14:15:22Z
+ DeploymentUndeploying:
+ summary: Undeploy initiated (transitional)
+ description: Asynchronous operation accepted; poll the deployment until status becomes UNDEPLOYED or FAILED.
+ value:
+ deploymentId: a73c85a1-d857-491e-a6b2-51dce05de7a2
+ name: v1.0-production
+ gatewayId: prod-gateway-01
+ status: UNDEPLOYING
+ baseDeploymentId: be6d8692-b9de-400e-b6c1-14db50154e27
+ metadata: {}
+ createdAt: 2019-08-24T14:15:22Z
+ updatedAt: 2019-08-24T14:15:22Z
GatewayNotFoundExample:
value:
status: error
@@ -8601,7 +8805,7 @@ components:
in: path
required: true
description: |
- **Project ID** (handle — unique slug identifier) of the Project.
+ **Project ID** consisting of the **handle** (unique slug identifier) of the Project.
schema:
type: string
example: "default-project"
@@ -8662,7 +8866,7 @@ components:
in: query
required: true
description: |
- **Project ID** consisting of the **handle** (unique slug identifier) of the Project to filter APIs by.
+ **Project ID** consisting of the **handle** (unique slug identifier) of the Project whose resources should be returned.
schema:
type: string
example: "default-project"
@@ -8672,17 +8876,20 @@ components:
in: query
required: false
description: |
- **Gateway ID** consisting of the **UUID** of the Gateway to filter status by.
+ **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway to filter status by.
schema:
type: string
- example: "987e6543-e21b-45d3-a789-426614174999"
+ pattern: '^[a-z0-9-]+$'
+ minLength: 3
+ maxLength: 63
+ example: "prod-gateway-01"
gatewayId:
name: gatewayId
in: path
required: true
description: |
- **Gateway ID** (handle — unique slug identifier) of the Gateway.
+ **Gateway ID** consisting of the **handle** (unique slug identifier) of the Gateway.
schema:
type: string
example: "prod-gateway-01"
@@ -8805,6 +9012,8 @@ tags:
description: Gateway registration and management operations
- name: Gateway Tokens
description: Gateway token rotation and revocation operations
+ - name: Gateway Policies
+ description: Custom gateway policy management operations
- name: LLM Providers
description: LLM provider management operations
- name: LLM Proxies
@@ -8813,6 +9022,8 @@ tags:
description: LLM provider template management operations
- name: LLM Provider Deployments
description: LLM provider deployment operations
+ - name: LLM Proxy Deployments
+ description: LLM proxy deployment operations
- name: API Keys
description: API key management operations for REST APIs and LLM Providers
- name: MCP Proxies
@@ -8821,5 +9032,9 @@ tags:
description: MCP proxy deployment operations
- name: Deployments
description: Deployment operations for various artifact types
+ - name: SubscriptionPlans
+ description: Subscription plan management operations — rate-limit tiers that subscriptions are created against
+ - name: Subscriptions
+ description: Subscription management operations linking applications to APIs under a subscription plan
- name: Secrets
description: Encrypted secret management — create, rotate, and delete organization-scoped secrets referenced via {{ secret "name" }} placeholders
\ No newline at end of file
diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/quickStart/lllmStepBanner/LLLMStepBanner.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/quickStart/lllmStepBanner/LLLMStepBanner.tsx
index b409e8fff8..f943d6ffec 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/quickStart/lllmStepBanner/LLLMStepBanner.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/quickStart/lllmStepBanner/LLLMStepBanner.tsx
@@ -133,9 +133,13 @@ export default function LLLMStepBanner({
}
setStepCompletion({
+ // A deployment record alone doesn't mean the provider is live — a
+ // failed or in-progress deploy must leave the step incomplete.
hasDeployments:
deploymentsResult.status === 'fulfilled'
- ? (deploymentsResult.value.list?.length ?? 0) > 0
+ ? (deploymentsResult.value.list ?? []).some(
+ (deployment) => deployment.status === 'DEPLOYED'
+ )
: false,
hasConsumptions:
apiKeysResult.status === 'fulfilled'
diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ProviderMap/ProviderMapTab.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ProviderMap/ProviderMapTab.tsx
index 8127e62cb6..3de7720853 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ProviderMap/ProviderMapTab.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ProviderMap/ProviderMapTab.tsx
@@ -53,7 +53,13 @@ import type { Gateway } from '../../../../../apis/gatewayTypes';
import type { DeploymentResponse, Proxy } from '../../../../../utils/types';
import { PLATFORM_API_BASE_URL } from '../../../../../config.env';
import { logger } from '../../../../../utils/logger';
-import { buildProjectPath } from '../../../../../utils/projectRouting';
+import {
+ buildGatewayPath,
+ buildProxyPath,
+} from '../../../../../utils/projectRouting';
+import { useAppAuth } from '../../../../../contexts/AppAuthContext';
+import { SCOPES } from '../../../../../auth/permissions';
+import useAIWorkspaceSnackbar from '../../../../../hooks/aiWorkspaceSnackbar';
import AnthropicLogo from '../../../../../assets/brands/Anthropic.jpg';
import AWSBedrockLogo from '../../../../../assets/brands/AWSBedrock.webp';
@@ -306,13 +312,28 @@ export function ProxyCard({
interface GatewayCardProps {
gateway: Gateway;
deployment?: DeploymentResponse;
+ onClick?: () => void;
}
-export function GatewayCard({ gateway, deployment }: GatewayCardProps) {
+export function GatewayCard({ gateway, deployment, onClick }: GatewayCardProps) {
const isDeployed = deployment?.status === 'DEPLOYED';
return (
-
+ {
+ if (!onClick) return;
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ onClick();
+ }
+ }}
+ role={onClick ? 'button' : undefined}
+ tabIndex={onClick ? 0 : undefined}
+ sx={{
+ cursor: onClick ? 'pointer' : undefined,
+ }}
+ >
([]);
const [proxyDeployments, setProxyDeployments] = useState<
@@ -649,27 +674,71 @@ export default function ProviderMapTab() {
void loadData();
}, [loadData]);
+ // A proxy lives inside a project, so there is no meaningful org-level route to
+ // fall back to: if the project can't be resolved, stay put and say why rather
+ // than navigating somewhere the proxy isn't.
const handleProxyClick = useCallback(
(proxyId: string, proxyProjectId?: string) => {
- // removed: ProjectBase no longer carries a `handler` alias field, so
- // match on `id` only.
- const proxyProject = projectsForCurrentOrganization.find(
- (project) => project.id === proxyProjectId
+ if (!currentOrganization) {
+ logger.error(
+ `Unable to navigate to proxy ${proxyId} because the current organization is unavailable.`
+ );
+ showSnackbar(
+ 'Unable to open this proxy right now. Please refresh and try again.',
+ 'error'
+ );
+ return;
+ }
+
+ const proxyPath = buildProxyPath(
+ currentOrganization,
+ projectsForCurrentOrganization,
+ proxyId,
+ proxyProjectId
);
- if (!currentOrganization || !proxyProject) {
+ if (!proxyPath) {
logger.error(
`Unable to navigate to proxy ${proxyId} because project ${
proxyProjectId ?? ''
} is unavailable.`
);
+ showSnackbar(
+ "Unable to open this proxy because its project isn't available to you.",
+ 'error'
+ );
+ return;
+ }
+
+ navigate(proxyPath);
+ },
+ [
+ currentOrganization,
+ navigate,
+ projectsForCurrentOrganization,
+ showSnackbar,
+ ]
+ );
+
+ // Gateways are organization-scoped, so unlike a proxy there is no project to
+ // resolve. Non-readers get a non-clickable card rather than a click that
+ // lands on a view they can't load.
+ const handleGatewayClick = useCallback(
+ (gatewayId: string) => {
+ if (!currentOrganization) {
+ logger.error(
+ `Unable to navigate to gateway ${gatewayId} because the current organization is unavailable.`
+ );
+ showSnackbar(
+ 'Unable to open this gateway right now. Please refresh and try again.',
+ 'error'
+ );
return;
}
- const proxyPath = `/proxies/${encodeURIComponent(proxyId)}`;
- navigate(buildProjectPath(currentOrganization, proxyProject, proxyPath));
+ navigate(buildGatewayPath(currentOrganization, gatewayId));
},
- [currentOrganization, navigate, projectsForCurrentOrganization]
+ [currentOrganization, navigate, showSnackbar]
);
const deploymentsByGateway = deployments.reduce<
@@ -765,7 +834,7 @@ export default function ProviderMapTab() {
proxy={proxy}
deployment={proxyId ? proxyDeployments[proxyId] : undefined}
onClick={
- proxyId
+ proxyId && canViewProxies
? () => handleProxyClick(proxyId, proxy.projectId)
: undefined
}
@@ -810,6 +879,11 @@ export default function ProviderMapTab() {
key={gw.id}
gateway={gw}
deployment={deploymentsByGateway[gw.id]}
+ onClick={
+ canViewGateways
+ ? () => handleGatewayClick(gw.id)
+ : undefined
+ }
/>
))
)}
diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ResourceDrawerCards.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ResourceDrawerCards.tsx
index 5a719d7af1..4296d4eedf 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ResourceDrawerCards.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ResourceDrawerCards.tsx
@@ -41,7 +41,10 @@ type ResourceDrawerCardsProps = {
gateways: Gateway[];
gatewayDeployments: Record;
proxyDeployments: Record;
- onProxyClick: (proxyId: string, proxyProjectId?: string) => void;
+ /** Omitted when the user can't read proxies — cards then render unclickable. */
+ onProxyClick?: (proxyId: string, proxyProjectId?: string) => void;
+ /** Omitted when the user can't read gateways — cards then render unclickable. */
+ onGatewayClick?: (gatewayId: string) => void;
onCreateProxy?: () => void;
onBlockedNavigation?: () => void;
};
@@ -52,6 +55,7 @@ export default function ResourceDrawerCards({
gatewayDeployments,
proxyDeployments,
onProxyClick,
+ onGatewayClick,
onCreateProxy,
onBlockedNavigation,
}: ResourceDrawerCardsProps) {
@@ -222,7 +226,7 @@ export default function ResourceDrawerCards({
proxyId ? proxyDeployments[proxyId] : undefined
}
onClick={
- proxyId
+ proxyId && onProxyClick
? () => onProxyClick(proxyId, proxy.projectId)
: undefined
}
@@ -242,6 +246,9 @@ export default function ResourceDrawerCards({
key={gw.id}
gateway={gw}
deployment={gatewayDeployments[gw.id]}
+ onClick={
+ onGatewayClick ? () => onGatewayClick(gw.id) : undefined
+ }
/>
))
))}
diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverviewTab.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverviewTab.tsx
index b14dcd1945..dc4bd8e4e9 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverviewTab.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverviewTab.tsx
@@ -66,7 +66,12 @@ import NoData from '../../../../assets/images/NoData.svg';
import { FormattedMessage } from 'react-intl';
import useAIWorkspaceSnackbar from '../../../../hooks/aiWorkspaceSnackbar';
import SwaggerSpecViewer from '../../../../Components/SwaggerSpecViewer';
-import { buildProjectPath } from '../../../../utils/projectRouting';
+import {
+ buildGatewayPath,
+ buildProxyPath,
+} from '../../../../utils/projectRouting';
+import { useAppAuth } from '../../../../contexts/AppAuthContext';
+import { SCOPES } from '../../../../auth/permissions';
import {
formatPrefixedKey,
resolveApiKeyAuthDisplay,
@@ -136,6 +141,9 @@ export default function ServiceProviderOverviewTab({
const { provider, getProviderAPIKeys } = useLLMProvider();
const { currentOrganization, projectsForCurrentOrganization } = useAppShell();
const navigate = useNavigate();
+ const { hasPermission } = useAppAuth();
+ const canViewGateways = hasPermission(SCOPES.GATEWAY_READ);
+ const canViewProxies = hasPermission(SCOPES.LLM_PROXY_READ);
const fetchedApiKeysProviderIdRef = useRef(null);
const fetchingApiKeysProviderIdRef = useRef(null);
const [gateways, setGateways] = useState([]);
@@ -590,27 +598,71 @@ export default function ServiceProviderOverviewTab({
}
};
+ // A proxy lives inside a project, so there is no meaningful org-level route to
+ // fall back to: if the project can't be resolved, stay put and say why rather
+ // than navigating somewhere the proxy isn't.
const handleProxyClick = useCallback(
(proxyId: string, proxyProjectId?: string) => {
- // removed: ProjectBase no longer carries a `handler` alias field, so
- // match on `id` only.
- const proxyProject = projectsForCurrentOrganization.find(
- (project) => project.id === proxyProjectId
+ if (!currentOrganization) {
+ logger.error(
+ `Unable to navigate to proxy ${proxyId} because the current organization is unavailable.`
+ );
+ showSnackbar(
+ 'Unable to open this proxy right now. Please refresh and try again.',
+ 'error'
+ );
+ return;
+ }
+
+ const proxyPath = buildProxyPath(
+ currentOrganization,
+ projectsForCurrentOrganization,
+ proxyId,
+ proxyProjectId
);
- if (!currentOrganization || !proxyProject) {
+ if (!proxyPath) {
logger.error(
`Unable to navigate to proxy ${proxyId} because project ${
proxyProjectId ?? ''
} is unavailable.`
);
+ showSnackbar(
+ "Unable to open this proxy because its project isn't available to you.",
+ 'error'
+ );
+ return;
+ }
+
+ navigate(proxyPath);
+ },
+ [
+ currentOrganization,
+ navigate,
+ projectsForCurrentOrganization,
+ showSnackbar,
+ ]
+ );
+
+ // Gateways are organization-scoped, so unlike a proxy there is no project to
+ // resolve. Non-readers get a non-clickable card rather than a click that
+ // lands on a view they can't load.
+ const handleGatewayClick = useCallback(
+ (gatewayId: string) => {
+ if (!currentOrganization) {
+ logger.error(
+ `Unable to navigate to gateway ${gatewayId} because the current organization is unavailable.`
+ );
+ showSnackbar(
+ 'Unable to open this gateway right now. Please refresh and try again.',
+ 'error'
+ );
return;
}
- const proxyPath = `/proxies/${encodeURIComponent(proxyId)}`;
- navigate(buildProjectPath(currentOrganization, proxyProject, proxyPath));
+ navigate(buildGatewayPath(currentOrganization, gatewayId));
},
- [currentOrganization, navigate, projectsForCurrentOrganization]
+ [currentOrganization, navigate, showSnackbar]
);
const handleDeleteApiKey = async () => {
@@ -656,7 +708,8 @@ export default function ServiceProviderOverviewTab({
gateways={gateways}
gatewayDeployments={gatewayDeployments}
proxyDeployments={proxyDeployments}
- onProxyClick={handleProxyClick}
+ onProxyClick={canViewProxies ? handleProxyClick : undefined}
+ onGatewayClick={canViewGateways ? handleGatewayClick : undefined}
onCreateProxy={onCreateProxy}
onBlockedNavigation={onBlockedNavigation}
/>
diff --git a/portals/ai-workspace/src/utils/projectRouting.ts b/portals/ai-workspace/src/utils/projectRouting.ts
index ca708ca881..341b5ba3c8 100644
--- a/portals/ai-workspace/src/utils/projectRouting.ts
+++ b/portals/ai-workspace/src/utils/projectRouting.ts
@@ -72,6 +72,51 @@ export function buildOrgPath(org: OrgLike | null | undefined, path: string): str
return `${base}/${path.replace(/^\/+/, '')}`;
}
+/**
+ * Resolves the route for a proxy that may live in any project of the current
+ * organization.
+ *
+ * `proxyProjectId` is the project handle (the same value `ProjectBase.id`
+ * carries), so the proxy's own project is matched by handle. Returns `null`
+ * when the proxy names a project outside `projects` — a stale listing, or a
+ * project the user can't see. Callers must surface that as an error rather
+ * than navigating somewhere the proxy doesn't live: an org-level fallback
+ * lands on a page whose project-scoped breadcrumbs and back links are wrong,
+ * and whose fetch fails anyway when the project is genuinely out of reach.
+ */
+export function buildProxyPath(
+ org: OrgLike | null | undefined,
+ projects: ProjectLike[],
+ proxyId: string,
+ proxyProjectId?: string
+): string | null {
+ const proxyProject = proxyProjectId
+ ? projects.find((project) => project.id === proxyProjectId)
+ : undefined;
+ if (!proxyProject) return null;
+
+ return buildProjectPath(
+ org,
+ proxyProject,
+ `/proxies/${encodeURIComponent(proxyId)}`
+ );
+}
+
+/**
+ * Resolves the route for a gateway detail view.
+ *
+ * Gateways are organization-scoped, not project-scoped, so this is always an
+ * org-level path. `gatewayId` is the gateway handle — the same value
+ * `Gateway.id` carries and the value `ViewGateway` matches its `:gatewayName`
+ * param against.
+ */
+export function buildGatewayPath(
+ org: OrgLike | null | undefined,
+ gatewayId: string
+): string {
+ return buildOrgPath(org, `/gateways/view/${encodeURIComponent(gatewayId)}`);
+}
+
export function buildProjectPath(
org: OrgLike | null | undefined,
project: ProjectLike | null | undefined,
diff --git a/tools/apidocs/widdershins_templates/authentication.def b/tools/apidocs/widdershins_templates/authentication.def
index f913266991..94e5472dca 100644
--- a/tools/apidocs/widdershins_templates/authentication.def
+++ b/tools/apidocs/widdershins_templates/authentication.def
@@ -1,3 +1,34 @@
+{{ var _schemes = (data.api.components && data.api.components.securitySchemes) || {}; }}
+{{ var _reqs = data.security || []; }}
+{{ var _isBasic = false; var _isBearer = false; var _scopes = []; }}
+{{ for (var _r = 0; _r < _reqs.length; _r++) {
+ var _req = _reqs[_r];
+ for (var _name in _req) {
+ var _sd = _schemes[_name] || {};
+ if (_sd.type === 'http' && (_sd.scheme || '').toLowerCase() === 'basic') { _isBasic = true; }
+ if (_sd.type === 'http' && (_sd.scheme || '').toLowerCase() === 'bearer') { _isBearer = true; }
+ if (_sd.type === 'oauth2' || _sd.type === 'openIdConnect') {
+ _isBearer = true;
+ var _reqScopes = _req[_name] || [];
+ for (var _s = 0; _s < _reqScopes.length; _s++) {
+ if (_scopes.indexOf(_reqScopes[_s]) < 0) { _scopes.push(_reqScopes[_s]); }
+ }
+ }
+ }
+ }
+}}
+{{? _isBearer && !_isBasic }}
+
+{{??}}
+{{?}}
diff --git a/tools/apidocs/widdershins_templates/code_shell.dot b/tools/apidocs/widdershins_templates/code_shell.dot
index 3cce32acf4..e9924929cf 100644
--- a/tools/apidocs/widdershins_templates/code_shell.dot
+++ b/tools/apidocs/widdershins_templates/code_shell.dot
@@ -1,7 +1,34 @@
{{ var defined_ct = data.allHeaders.find(function(h){ return h.name === 'Content-Type'; }); }}
{{ if (defined_ct && data.consumes.indexOf('application/json') >= 0) { defined_ct.exampleValues.object = 'application/json'; } }}
{{ var ext = 'json'; if (defined_ct) { var ct = defined_ct.exampleValues.object; if (ct.indexOf('yaml') >= 0) ext = 'yaml'; else if (ct.indexOf('xml') >= 0) ext = 'xml'; else if (ct.indexOf('text') >= 0) ext = 'txt'; } }}
+{{ var _schemes = (data.api.components && data.api.components.securitySchemes) || {}; }}
+{{ var _reqs = data.operation.security ? data.operation.security : (data.api.security || []); }}
+{{ var _isBasic = false; var _isBearer = false; }}
+{{ for (var _r = 0; _r < _reqs.length; _r++) {
+ var _req = _reqs[_r];
+ for (var _name in _req) {
+ var _sd = _schemes[_name] || {};
+ if (_sd.type === 'http' && (_sd.scheme || '').toLowerCase() === 'basic') { _isBasic = true; }
+ if (_sd.type === 'http' && (_sd.scheme || '').toLowerCase() === 'bearer') { _isBearer = true; }
+ if (_sd.type === 'oauth2' || _sd.type === 'openIdConnect') { _isBearer = true; }
+ }
+ }
+}}
+{{ var _hdrs = data.allHeaders.filter(function(h){ return h.name.toLowerCase() !== 'authorization'; }); }}
+{{ var _isMultipart = data.consumes.indexOf('multipart/form-data') >= 0 && data.consumes.indexOf('application/json') < 0; }}
+{{ var _mpFields = []; }}
+{{ if (_isMultipart) {
+ var _ex = data.bodyParameter && data.bodyParameter.exampleValues && data.bodyParameter.exampleValues.object;
+ if (_ex && typeof _ex === 'object') {
+ for (var _k in _ex) {
+ var _v = _ex[_k];
+ if (_v !== null && _v !== undefined && typeof _v !== 'object') { _mpFields.push(_k + '=' + _v); }
+ }
+ }
+ }
+}}
curl -X {{=data.methodUpper}} {{=data.url}}{{=data.requiredQueryString}} \
- -u {username}:{password}{{?data.allHeaders.length || data.operation.requestBody}} \{{?}}
-{{~data.allHeaders :p:index}} -H '{{=p.name}}: {{=p.exampleValues.object}}'{{?index < data.allHeaders.length-1 || data.operation.requestBody}} \{{?}}
-{{~}}{{?data.operation.requestBody}} -d @payload.{{=ext}}{{?}}
+{{? _isBearer && !_isBasic }} -H 'Authorization: Bearer {access_token}'{{??}} -u {username}:{password}{{?}}{{?_hdrs.length || data.operation.requestBody}} \{{?}}
+{{~_hdrs :p:index}} -H '{{=p.name}}: {{=p.exampleValues.object}}'{{?index < _hdrs.length-1 || data.operation.requestBody}} \{{?}}
+{{~}}{{?data.operation.requestBody}}{{?_mpFields.length}}{{~_mpFields :_f:_fi}} -F '{{=_f}}'{{?_fi < _mpFields.length-1}} \{{?}}
+{{~}}{{??}} -d @payload.{{=ext}}{{?}}{{?}}
diff --git a/tools/apidocs/widdershins_templates/operation.dot b/tools/apidocs/widdershins_templates/operation.dot
index 9f519e83de..2a4dd7dd02 100644
--- a/tools/apidocs/widdershins_templates/operation.dot
+++ b/tools/apidocs/widdershins_templates/operation.dot
@@ -27,7 +27,8 @@
{{? data.operation.description}}{{= data.operation.description }}{{?}}
-{{? data.operation.requestBody}}
+{{ var _mpOnly = data.consumes && data.consumes.indexOf('multipart/form-data') >= 0 && data.consumes.indexOf('application/json') < 0; }}
+{{? data.operation.requestBody && !_mpOnly}}
> Payload
{{? data.bodyParameter.exampleValues.description }}
diff --git a/tools/apidocs/widdershins_templates/responses.def b/tools/apidocs/widdershins_templates/responses.def
index ac9679efde..66378a781d 100644
--- a/tools/apidocs/widdershins_templates/responses.def
+++ b/tools/apidocs/widdershins_templates/responses.def
@@ -6,7 +6,7 @@
{{? data.responseSchemas }}
> Example responses
-
+>
{{= data.utils.getResponseExamples(data) }}
{{?}}