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:

Universal API request pipeline
Access control

The API must be mounted on the location the request arrived through, and the caller must satisfy its allow list — 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 as req.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:

src/main/resources/apis/<api-name>/<api-name>.ts
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.

WebSocket and SSE

A custom API can act as a WebSocket channel or a Server-Sent Events stream, like any other HTTP function in XP. Return a webSocket or sse object from the request handler. See WebSocket and SSE for the details.

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:

src/main/resources/apis/<api-name>/<api-name>.yaml
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:

  1. Web endpoint — the API opts in with mount: "web" and becomes reachable on the public XP port under :8080/api/.

  2. Contextual — the API is mounted into a site, webapp, or admin tool service via that service’s descriptor, and is reachable under that service’s /_/<app>:<api> path.

  3. 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 allowedPrincipals field 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 site service: /site/<site-path>/_/<app>:<api>

  • For the webapp service: /webapp/<webapp-app>/_/<app>:<api>

  • For the admin service: /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:

  1. The application providing the site descriptor must be configured on the site instance. Without this, the site descriptor’s apis list is not consulted at all for that site.

  2. That application’s site descriptor must list the API in its apis field.

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:

src/main/resources/cms/site.yaml
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:

src/main/resources/apis/myapi/myapi.yaml
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


Contents

Contents