Universal APIs
Contents
A Universal API is a custom HTTP API shipped in an application: a TypeScript module and a descriptor under src/main/resources/apis/. Installing the application adds the API to the instance, and the same implementation can then be exposed on the Web endpoint, inside a site, webapp, or admin tool, or on the management port — including from other applications, which may borrow it by name. Nothing is mounted or reachable until the descriptor says so, and an API mounts at a single, fixed location per endpoint, never at arbitrary points along a URL.
Request pipeline
A Universal API is not tied to a single URL — the same implementation can be reached from the Web endpoint (/api/<app>:<api>), as a contextual API inside the site, webapp, and admin services (/_/<app>:<api>), or on the Management endpoint (/<app>:<api>). However it arrives, every request is gated the same way:
- Access control
-
The API must be mounted on the location the request arrived through, and the caller must satisfy its
allowlist — on every request, regardless of endpoint. The descriptor section lists the resulting status codes. -
<api-name>.ts -
The matched API’s exported function for the request method (
GET,POST, …) handles the request; subpaths arrive asreq.path.
See Where APIs are reachable for the rules governing each mount point.
Usage
To create an API, place an implementation under src/main/resources/apis/ in a folder matching its name, e.g. src/main/resources/apis/myapi/myapi.ts.
Example:
export function GET(req) {
return {
body: {
time: new Date()
},
contentType: 'application/json; charset=utf-8'
};
}
Subpath routing
A request to /api/<app>:<api>/foo/bar (or the equivalent contextual URL) reaches the same implementation; everything after the descriptor segment arrives as req.path. The implementation is free to dispatch on req.path any way it likes, but for anything beyond a couple of static endpoints, use the router library for method dispatch, parameter extraction, and pattern matching.
Descriptor
An API descriptor is required. It defines the API’s access control, the endpoints it is mounted on, and metadata such as title, description, and documentation URL.
Example of a full API descriptor:
kind: "API"
title: "My API" (1)
description: "API for My App" (2)
documentationUrl: "https://developer.enonic.com/docs/api" (3)
mount: ["web"] (4)
allow: (5)
- "role:system.admin"
- "role:myapp.myrole"
| 1 | title is the name of the API that will be shown in the UI. |
| 2 | description is a short description of the API that will be shown in the UI. |
| 3 | documentationUrl is a link to the API documentation that will be shown in the UI. |
| 4 | mount lists the endpoints the API is exposed on: "web" for the Web endpoint, "management" for the Management endpoint, or both. APIs are mounted nowhere by default. |
| 5 | APIs must list principals that have access to it. It is required to specify at least one principal. Use role:system.everyone to make the API public. |
A request to an API that is not mounted on the matched URL returns 404 Not Found — the absence is indistinguishable from a missing API. A request by a principal not in the allow list returns 401 Unauthorized if the configured ID provider asks for credentials, otherwise 403 Forbidden. Calls to APIs mounted under /admin/… additionally require the caller to hold role:system.admin.login, regardless of what the API’s allow list grants. |
Where APIs are reachable
A Universal API can be exposed through three independent axes — they don’t substitute for each other and any combination is valid:
-
Web endpoint — the API opts in with
mount: "web"and becomes reachable on the public XP port under:8080/api/. -
Contextual — the API is mounted into a
site,webapp, oradmin toolservice via that service’s descriptor, and is reachable under that service’s/_/<app>:<api>path. -
Management endpoint — the API opts in with
mount: "management"and becomes reachable at the root of the Management endpoint,:4848/.
Web endpoint
The dedicated /api URL on the Web endpoint (the public XP port) exposes APIs independently of any service, so they can be consumed by any HTTP client:
-
/api/<app>:<api>
To opt in, an API must include "web" in its mount list (see API descriptor).
The port and the /api prefix are XP’s defaults. A deployment may expose the API on a custom domain and path with virtual hosts, so generate API URLs with apiUrl() rather than hardcoding them.
API discovery
GET /api returns a JSON index of every API with "web" in its mount list. The response is a resources array, one entry per API:
{
"resources": [
{
"descriptor": "com.example.app:myapi",
"application": "com.example.app",
"name": "myapi",
"title": "My API",
"description": "API for my app",
"documentationUrl": "https://example.com/docs",
"mount": ["web"],
"allowedPrincipals": ["role:system.admin", "role:myapp.role"]
}
]
}
Behavior worth knowing:
-
The Management endpoint serves no equivalent index.
-
Contextually mounted APIs (available only under sites, webapps, or admin tools) do not appear in the listing.
-
Both YAML-declared APIs and dynamically registered Java handlers are included.
-
The listing is not filtered by the caller’s principals — clients see every API mounted on the endpoint and are expected to compare against the
allowedPrincipalsfield themselves before invoking.
| Discovery is intended for development. By default it is enabled only when XP runs in dev mode and disabled in production. |
Contextual APIs
In the site, webapp, and admin services, APIs are mounted contextually under the service’s URL space. The _ segment separates the service’s content path from service paths (services, components, idprovider, and APIs):
-
For the
siteservice:/site/<site-path>/_/<app>:<api> -
For the
webappservice:/webapp/<webapp-app>/_/<app>:<api> -
For the
adminservice:/admin/<admin-app>/<tool>/_/<app>:<api>
Each service descriptor declares the APIs it exposes via its apis list. For webapp and admin this is sufficient — the descriptor of the hosting application controls the mount. For site, two conditions must both hold:
-
The application providing the site descriptor must be configured on the site instance. Without this, the site descriptor’s
apislist is not consulted at all for that site. -
That application’s site descriptor must list the API in its
apisfield.
The site mount is also limited to the site root: the path between <branch> and /_/ must be the site’s root content path. Deeper content paths like /site/<project>/<branch>/some/page/_/<app>:<api> do not resolve as APIs.
All three service descriptors use the same apis: list shape:
kind: "Site"
apis:
- "ws" (1)
- "app:graphql" (2)
| 1 | A bare name resolves to an API in the current application — here, ws defined in src/main/resources/apis/ws/. |
| 2 | The <app>:<api> form mounts an API from a different application, letting the service borrow APIs deployed by other apps. |
src/main/resources/webapp/webapp.yaml (kind: "WebApp") and src/main/resources/admin/tools/<tool-name>/<tool-name>.yaml (kind: "AdminTool") take the same apis: list.
| Wildcards and "expose all APIs from app X" rules are intentionally not supported here. Each API must be listed by its descriptor key. The reason is security: it prevents new or insecure APIs from being silently exposed on a service path when an app is upgraded or a new dependency is added. Publishing an additional API always requires an explicit change to the hosting descriptor. |
Management endpoint
An API may also be mounted on the Management endpoint. This is XP’s control plane — the port the Enonic CLI talks to for operations against the instance itself, such as deploying applications, snapshots, and reindexing, and what the endpoint is named after. It listens on 4848 by default and every request must be authenticated. The endpoint itself, its configuration, and the operations XP already exposes there are documented in the platform documentation.
Add "management" to the mount list to expose an API there:
kind: "API"
title: "Cluster ops"
mount: ["management"]
allow:
- "role:system.admin"
APIs on the Management endpoint are served from the root of the port — there is no /api prefix here:
-
:4848/myapp:myapi
Listing both mounts — mount: ["web", "management"] — makes the same API reachable on either port, each under its own URL form:
-
:8080/api/myapp:myapi -
:4848/myapp:myapi
The allow principal list is enforced on every request regardless of which endpoint served it.
apiUrl()
To safely generate an API URL, use the apiUrl() function from the Portal library. It picks the right URL form based on the calling context — a contextual URL under the active service when called from a site, webapp, or admin tool, and /api/ otherwise.
import {apiUrl} from '/lib/xp/portal';
const url = apiUrl({
api: 'com.example.myapp:myapi', (1)
path: '/items', (2)
params: { id: '42' } (3)
});
| 1 | Descriptor key in <app>:<api> form. Required. |
| 2 | Optional path appended after the API segment. Accepts a string or a string array. |
| 3 | Optional query parameters. |
Tracing
Each Universal API request emits a universalAPI trace event with the API descriptor key and response status, observable through XP’s standard tracing infrastructure.
Reserved API application names
Application names that collide with XP’s own endpoint segments under _ or with built-in APIs such as media:image are reserved, including:
-
media -
admin -
component -
attachment -
image -
asset -
service -
error -
idprovider