A lightweight, self-contained developer portal plugin for Otoroshi. It turns any Otoroshi API into a full-featured portal with documentation rendering, OpenAPI exploration, self-service API key subscription, and a built-in API tester -- all without deploying a separate frontend application.
If you expose APIs through Otoroshi and need a developer-facing portal, you typically have two options: build a custom frontend or integrate a heavy third-party solution. This plugin offers a third path:
- Zero infrastructure -- the portal is served directly by Otoroshi as a backend plugin. No extra server, no Node.js app, no static hosting to manage.
- Configuration-driven -- everything (pages, navigation, OpenAPI specs, subscription plans) is declared in the API's
documentationJSON section. Update the config, the portal updates instantly. - Self-service subscriptions -- authenticated users can subscribe to published plans, get API keys, and start calling your API immediately. Plans support auto-validation or manual approval workflows.
- Built-in API tester -- users can test endpoints directly from the portal using their own API keys, without leaving the browser.
- Dark mode, responsive, modern UI -- the portal ships with a polished Tailwind CSS interface that works on desktop and mobile out of the box.
- Home page with HTML or Markdown content
- Documentation pages with sidebar navigation (categories and links)
- Markdown rendering (via zero-md web component)
- OpenAPI specification rendering (via Scalar)
- Multiple OpenAPI spec support with a selection page
- Self-service API key subscription with plan selection
- API key management (create, update, delete, copy bearer token)
- Built-in API tester with request/response inspection
- Remote content loading (documentation pages and resources fetched from URLs)
- Remote portal configuration (load the entire
documentationsection from a URL) - Redirections
- Light / Dark / System theme with persistence
- Authentication integration via Otoroshi auth modules
- Otoroshi cluster support (leader/worker mode)
- Download the latest JAR from the releases page
- Add the JAR to your Otoroshi plugins directory (see Otoroshi documentation on loading external plugins)
- Restart Otoroshi -- the plugin
Otoroshi API Portalwill appear in the available plugins list
Using the portal requires two things: an API with a documentation section, and a Route that serves the portal.
In the Otoroshi admin, create or edit an API (apis.otoroshi.io/Api). Add a documentation section to it. Here is a minimal example:
{
"documentation": {
"enabled": true,
"home": {
"path": "/home",
"content_type": "text/html",
"site_page": true,
"transform": "markdown",
"text_content": "# Welcome\n\nThis is my API portal."
},
"logo": {
"url": "https://example.com/logo.png",
"path": "/favicon.png",
"content_type": "image/png"
},
"references": [
{
"title": "My API",
"link": "/openapi.json"
}
],
"resources": [
{
"title": "My API",
"path": "/openapi.json",
"content_type": "application/json",
"url": "https://example.com/openapi.json"
}
],
"navigation": [
{
"label": "Documentation",
"icon": { "css_icon_class": "bi bi-journal-text me-2" },
"path": "/documentation",
"items": [
{
"label": "API",
"kind": "category",
"links": [
{
"label": "API Reference",
"link": "/api-references",
"icon": { "css_icon_class": "bi bi-braces me-2" }
}
]
}
]
}
],
"plans": [
{
"id": "free",
"name": "Free",
"description": "A free plan to try the API",
"access_mode_configuration_type": "apikey",
"access_mode_configuration": {
"throttling_quota": 10,
"daily_quota": 100,
"monthly_quota": 1000
},
"status": "published",
"tags": [],
"metadata": {}
}
],
"redirections": [],
"search": { "enabled": true },
"metadata": {},
"tags": []
}
}Create an Otoroshi Route (proxy.otoroshi.io/Route) that will serve the portal. The route needs the following plugin chain:
{
"frontend": {
"domains": ["portal.example.com"],
"strip_path": true,
"exact": false
},
"backend": {
"targets": [
{
"hostname": "request.otoroshi.io",
"port": 443,
"tls": true
}
]
},
"plugins": [
{
"plugin": "cp:otoroshi.next.plugins.OverrideHost",
"enabled": true,
"config": {}
},
{
"plugin": "cp:otoroshi.next.plugins.NgAuthModuleUserExtractor",
"enabled": true,
"config": {
"module": "<your-auth-module-id>"
}
},
{
"plugin": "cp:otoroshi.next.plugins.AuthModule",
"enabled": true,
"include": ["/login"],
"config": {
"module": "<your-auth-module-id>"
}
},
{
"plugin": "cp:otoroshi_plugins.com.cloud.apim.plugins.apiportal.OtoroshiApiPortal",
"enabled": true,
"config": {
"api_ref": "<your-api-id>"
}
}
]
}The plugins serve different roles:
| Plugin | Role |
|---|---|
OverrideHost |
Rewrites the Host header for the backend target |
NgAuthModuleUserExtractor |
Extracts the authenticated user on every request (without blocking). This allows the portal to show/hide subscription features based on login state |
AuthModule (include: /login) |
Forces authentication only on the /login path. The portal remains publicly browsable; users log in explicitly to manage subscriptions |
OtoroshiApiPortal |
The portal plugin itself |
| Field | Type | Description |
|---|---|---|
api_ref |
string |
Required. The ID of the Otoroshi API whose documentation will be rendered |
prefix |
string |
Optional path prefix if the portal is not served at the root of the domain (e.g. /portal) |
The documentation section of the API is a JSON object with the following fields:
| Field | Type | Description |
|---|---|---|
enabled |
boolean |
Enable or disable the portal |
source |
object | null |
Load the entire documentation config from a remote URL. When set, the url field inside must point to a JSON file matching this schema. Useful for managing portal config in a git repository |
home |
resource |
The home page resource (displayed at /) |
logo |
resource |
The portal logo/favicon resource |
references |
reference[] |
List of OpenAPI specifications to expose |
resources |
resource[] |
All servable resources (pages, specs, images, etc.) |
navigation |
sidebar[] |
Top-level navigation tabs, each containing a sidebar with categories and links |
plans |
plan[] |
Subscription plans available to users |
redirections |
redirection[] |
URL redirections |
search |
object |
Search configuration ({ "enabled": true }) |
footer |
object | null |
Footer configuration (not yet implemented) |
banner |
object | null |
Banner configuration (not yet implemented) |
metadata |
object |
Arbitrary key-value metadata |
tags |
string[] |
Tags |
You can host your entire documentation configuration as a JSON file and load it remotely:
{
"documentation": {
"enabled": true,
"source": {
"url": "https://raw.githubusercontent.com/my-org/my-api/main/portal.config.json"
}
}
}The remote JSON file must follow the same documentation schema. This is useful for managing your portal configuration in version control alongside your API code.
A resource represents any servable content: a page, an image, a JSON file, etc.
| Field | Type | Description |
|---|---|---|
path |
string |
The URL path where this resource is served (e.g. /documentation/getting-started) |
title |
string |
Optional display title |
content_type |
string |
MIME type (text/html, text/markdown, application/json, image/png, etc.) |
site_page |
boolean |
If true, the resource is rendered inside the portal layout (header, sidebar, etc.). If false or absent, it is served raw |
text_content |
string |
Inline text content (HTML, Markdown, etc.) |
url |
string |
Fetch the content from this URL at render time. Takes precedence over text_content |
base64_content |
string |
Base64-encoded binary content (for images, etc.) |
json_content |
object |
Inline JSON content |
transform |
string |
Apply a transformation: "markdown" renders Markdown to HTML, "redoc" renders an OpenAPI spec with Redoc |
transform_wrapper |
string |
HTML wrapper around transformed content. Use {content} as a placeholder (e.g. <div class="container">{content}</div>) |
Content resolution priority: url > base64_content > json_content > text_content.
A reference points to an OpenAPI specification that will be rendered with Scalar in the API reference section.
| Field | Type | Description |
|---|---|---|
title |
string |
Display name of the specification |
link |
string |
Path to the corresponding resource (e.g. /openapi.json). Must match a resource's path |
description |
string |
Optional description shown on the reference selection page |
icon |
object |
Optional icon ({ "css_icon_class": "bi bi-rocket" }) |
If you have a single reference, the API reference page shows the spec directly. With multiple references, a selection page is displayed first.
Navigation entries appear as tabs in the top navigation bar. Each entry contains a sidebar with categories and links for its section.
{
"label": "Documentation",
"icon": { "css_icon_class": "bi bi-journal-text me-2" },
"path": "/documentation",
"items": [
{
"label": "Guides",
"kind": "category",
"links": [
{
"label": "Getting started",
"link": "/documentation/getting-started",
"icon": { "css_icon_class": "bi bi-book me-2" }
}
]
},
{
"label": "FAQ",
"link": "/documentation/faq",
"icon": { "css_icon_class": "bi bi-question-circle me-2" }
}
]
}Items can be either categories (with "kind": "category" and a links array) or direct links (with a link field). Icons use Bootstrap Icons CSS classes.
Plans define the subscription options available to authenticated users.
| Field | Type | Description |
|---|---|---|
id |
string |
Unique identifier for the plan |
name |
string |
Display name |
description |
string |
Description shown to users when choosing a plan |
access_mode_configuration_type |
string |
Type of access: "apikey" for API key-based access |
access_mode_configuration |
object |
Quotas: throttling_quota (req/sec), daily_quota, monthly_quota |
status |
string |
"published" to make the plan available, any other value hides it |
tags |
string[] |
Tags applied to generated API keys |
metadata |
object |
Metadata applied to generated API keys |
Plans with access_mode_configuration_type: "apikey" enable the Subscriptions tab in the portal navigation for logged-in users. When a user subscribes, an API key is generated with the configured quotas and authorized for the API.
{
"from": "/old-path",
"to": "/new-path"
}A /logout -> /.well-known/otoroshi/logout redirection is always added automatically.
Here is a complete documentation section for a Wine API portal with multiple documentation pages, a single OpenAPI spec, and a subscription plan:
{
"enabled": true,
"home": {
"path": "/home",
"content_type": "text/html",
"site_page": true,
"transform": "markdown",
"transform_wrapper": "<div class=\"container-xxl\" style=\"margin-top: 30px;\">{content}</div>",
"text_content": "<div class=\"container-xxl\" style=\"margin-top: 30px;\">\n\n# Welcome to the Wine API\n\nThe **Wine API** gives you access to a rich catalog of wines, grape varieties, wineries, and wine regions.\n\n## Getting Started\n\n1. **Sign up** for an API key on this portal.\n2. **Check out the documentation** to learn about available endpoints.\n3. **Start querying**:\n\n ```bash\n curl https://wines-api.example.com/api/wines?region=Bordeaux\n ```\n\nGo to the [documentation](/documentation) for more details.\n</div>\n"
},
"logo": {
"url": "https://example.com/wine-logo.png",
"path": "/favicon.png",
"content_type": "image/png"
},
"references": [
{
"title": "Wines API",
"link": "/openapi.json"
}
],
"resources": [
{
"title": "Wines API",
"path": "/openapi.json",
"content_type": "application/json",
"url": "https://wines-api.example.com/docs/openapi.json"
},
{
"path": "/documentation/getting-started",
"content_type": "text/html",
"site_page": true,
"transform": "markdown",
"url": "https://raw.githubusercontent.com/my-org/wines-api/main/docs/getting-started.md"
},
{
"path": "/documentation/latency",
"content_type": "text/markdown",
"site_page": true,
"transform": "markdown",
"url": "https://raw.githubusercontent.com/my-org/wines-api/main/docs/latency.md"
},
{
"path": "/docs/architecture.png",
"content_type": "image/png",
"url": "https://raw.githubusercontent.com/my-org/wines-api/main/docs/architecture.png"
}
],
"navigation": [
{
"label": "Documentation",
"icon": { "css_icon_class": "bi bi-journal-text me-2" },
"path": "/documentation",
"items": [
{
"label": "Guides",
"kind": "category",
"links": [
{
"label": "Getting started",
"link": "/documentation/getting-started",
"icon": { "css_icon_class": "bi bi-book me-2" }
},
{
"label": "Latency",
"link": "/documentation/latency",
"icon": { "css_icon_class": "bi bi-speedometer2 me-2" }
}
]
},
{
"label": "API",
"kind": "category",
"links": [
{
"label": "API Reference",
"link": "/api-references",
"icon": { "css_icon_class": "bi bi-braces me-2" }
}
]
}
]
}
],
"plans": [
{
"id": "dev",
"name": "Dev",
"description": "An API key to try the API on prototypes",
"access_mode_configuration_type": "apikey",
"access_mode_configuration": {
"throttling_quota": 100,
"daily_quota": 1000,
"monthly_quota": 10000
},
"status": "published",
"tags": [],
"metadata": {
"env": "dev"
}
}
],
"redirections": [
{ "from": "/docs", "to": "/documentation" }
],
"search": { "enabled": true },
"footer": null,
"banner": null,
"metadata": {},
"tags": []
}The portal exposes the following routes internally:
| Method | Path | Description |
|---|---|---|
GET |
/ |
Home page |
GET |
/login |
Triggers authentication (redirects to auth module) |
GET |
/logout |
Logs out (redirected to Otoroshi logout) |
GET |
/api-references |
OpenAPI reference page (or spec selection if multiple) |
GET |
/api-references/<spec-path> |
Specific OpenAPI spec rendering |
GET |
/subscriptions |
API key management page (requires authentication) |
GET |
/portal.js |
Portal JavaScript (theme, modals, API tester logic) |
GET |
/<resource-path> |
Any resource defined in the documentation |
GET |
/api/plans |
JSON list of available plans |
GET |
/api/documentation |
JSON documentation metadata |
GET |
/api/apikeys |
JSON list of current user's API keys |
POST |
/api/apikeys |
Create a new API key |
PUT |
/api/apikeys/<client_id> |
Update an API key |
DELETE |
/api/apikeys/<client_id> |
Delete an API key |
POST |
/api/_test |
Proxy a test request to any URL |
Apache 2.0 -- Copyright 2023-2025 Cloud APIM



