Skip to main content

Serve API reference

The serve API is the public, unauthenticated surface your applications talk to. Most endpoints answer from the active deployment of the resolved environment; the three file endpoints do not, which Which deployment answers sets out precisely.

All examples use <API_BASE>:

Setup<API_BASE>
Hosted consolehttps://console.mfe-orchestrator.dev/api
Self-hosted<FRONTEND_URL>/api, or BACKEND_URL if set

:::info No authentication These endpoints are public by design — they are called from browsers, where a credential could not be kept secret anyway. Do not put anything confidential in environment variables. The management API is a separate, authenticated surface. :::

Addressing an environment

Most endpoints come in several flavours, differing only in how the environment is identified:

FormEnvironment resolved from
.../<environmentId>Explicit object id
.../<projectId>/<environmentSlug>Explicit project + slug — more readable, stable
.../auto/<projectId>/... or /<mfeId>The request's Referer, matched against allowed domains

Prefer the slug form in configuration you write by hand, and the auto form when you want one build to work in every environment.

The auto form is only as reliable as the domain list behind it: the host's real domain has to be registered on exactly one environment of that project, otherwise the call answers 404 with code: "ENVIRONMENT_NOT_FOUND" and a message naming the domain it could not place. That is the trade the auto form makes — one artifact everywhere, in exchange for a piece of configuration that lives in the console rather than in your build.

Which deployment answers

Most of this API answers from the active deployment — the one flagged active on the environment, which is what a rollback or redeploy moves. Three endpoints do not:

EndpointDeployment it answers from
/serve/all/…Active
/serve/global-variables/…Active
/serve/mfe/config/…Active
/serve/mfe/files/{projectId}/{environmentSlug}/…Newest by deployedAt, active or not
/serve/mfe/files/auto/{projectId}/…Newest by createdAt, active or not
/serve/mfe/files/{mfeId}/…Newest by createdAt, active or not

The three file endpoints filter on no active flag, and the last two sort on the wrong field. A rollback re-activates an older deployment and stamps its deployedAt with the moment you rolled back; it does not touch createdAt. So after a rollback the deployedAt form serves the deployment you rolled back to, while the two createdAt forms keep serving the newest-created one — the very deployment you rolled away from.

:::note An SDK-wired host is on the safe route Every url in the manifest carries the environment slug, so a host that loads its remotes through remoteUrl() only ever requests the {projectId}/{environmentSlug} form, which sorts on deployedAt and therefore honours a rollback. The exposure is a consumer that hand-writes an auto/{projectId} or {mfeId} file URL. Prefer the slug form for anything you write by hand. :::

Everything about an environment

GET <API_BASE>/serve/all/{environmentId}
GET <API_BASE>/serve/all/{projectId}/{environmentSlug}
GET <API_BASE>/serve/all/auto/{projectId} # Referer or Host
?mfeSessionId=<uuid>&mfeDeviceId=<uuid>&mfeUserId=<optional>

Returns the whole environment in one call: microfrontends with resolved URLs, and variables.

The third form names no environment at all: the caller says only which project it belongs to, and the platform picks the environment whose allowed domains match the request. It is what the client SDK calls when configure() was given no environment, and what lets one host build serve every stage.

curl "<API_BASE>/serve/all/68f1a2.../prod"
{
"globalVariables": [
{ "key": "API_URL", "value": "https://api.example.com" }
],
"microfrontends": [
{
"url": "https://console.mfe-orchestrator.dev/api/serve/mfe/files/68f1.../prod/catalog/assets/remoteEntry.js",
"slug": "catalog",
"name": "Catalog",
"nameToIntegrate": "catalog",
"version": "1.4.0",
"continuousDeployment": false
},
{
"url": "https://console.mfe-orchestrator.dev/api/serve/mfe/files/68f1.../prod/checkout-new/_v/1.5.0-rc1/assets/remoteEntry.js",
"slug": "checkout-new",
"name": "Checkout",
"nameToIntegrate": "checkoutnew",
"version": "1.5.0-rc1",
"continuousDeployment": true
}
]
}

nameToIntegrate is the Module Federation remote name — the slug with every / replaced by _ and every - removed. Use it as the key when registering remotes dynamically:

SlugnameToIntegrate
catalogcatalog
product-catalogproductcatalog
shop/catalogshop_catalog

version is the version this response resolves to, which is not necessarily the deployment's version: a microfrontend on a Based on version canary reports the version this caller gets.

:::caution A Based on URL canary reports the stable version That branch has no version of the platform's own to pin — the canary is a URL somewhere else — so the entry carries the canary url next to the deployed version. A caller drawn into a URL canary is therefore told it is running the stable version. url is the field to trust there. :::

url is resolved, and already version-pinned when it needs to be. The second entry above is on a version-based canary, so the version appears as a _v/<version>/ path segment. Use the string as it is: never rebuild it, never strip that segment. The client SDK does this correctly for you.

The body has exactly the two keys shown above. It does not name the environment it resolved to — response.environment is undefined in all three forms — and it does not need to: the environment slug is baked into every url it hands out. So the auto form resolves the domain once, on this call, and the file requests that follow already carry their environment in the path. They work from a host whose domain is registered nowhere, and they are also the form that keeps answering from the right deployment after a rollback — see Which deployment answers.

Identity parameters

The three optional query parameters carry the identities a canary decision can be computed on:

ParameterIdentity
mfeDeviceIdThe browser — a Session canary buckets on this
mfeUserIdThe logged-in user — a User canary looks this up in its enrolment list
mfeSessionIdThe browsing session — no canary strategy is computed on it, it is there for your own telemetry

Send every one you have. They are not cookies and cannot be: module scripts are fetched with a fixed same-origin credentials mode, so the URL is the only channel — see canary releases. Omitting them is supported: a Session canary then falls back to a draw per page load instead of a sticky one, and a User canary serves the stable version to everyone.

This is the endpoint to build runtime discovery on: one request at boot gives you the full roster and the configuration.

Environment variables

GET <API_BASE>/serve/global-variables/{environmentId}
GET <API_BASE>/serve/global-variables/{projectId}/{environmentSlug}
GET <API_BASE>/serve/global-variables/auto/{projectId} # Referer or Host
GET <API_BASE>/serve/global-variables/{environmentId}/index.js
GET <API_BASE>/serve/global-variables/auto/{projectId}/index.js # Referer or Host

The first three return JSON; the last two return JavaScript that assigns window.globalConfig and is served as application/javascript, ready for a <script src> tag.

The auto form is the one to put in an index.html: it names no environment, so the same document works in every one of them.

See Runtime configuration for usage.

One microfrontend's configuration

GET <API_BASE>/serve/mfe/config/{projectId}/{environmentSlug}/{mfeSlug}
GET <API_BASE>/serve/mfe/config/{environmentId}/{mfeSlug}
GET <API_BASE>/serve/mfe/config/auto/{projectId}/{mfeSlug} # Referer or Host
GET <API_BASE>/serve/mfe/config/{mfeId} # Referer required

Returns a single entry in the same shape as the items in microfrontends above — the resolved URL and the version that entry resolves to, which is the canary-resolved one wherever a version canary applies, with the same Based on URL caveat.

Useful when a host wants to look up one remote lazily rather than fetching the whole environment.

Microfrontend files

These endpoints stream the actual bundle files, and are what the URLs in your Module Federation configuration point at.

GET <API_BASE>/serve/mfe/files/{projectId}/{environmentSlug}/{mfeSlug}/{path}
GET <API_BASE>/serve/mfe/files/auto/{projectId}/{mfeSlug}/{path} # Referer or Host
GET <API_BASE>/serve/mfe/files/{mfeId}/{path} # Referer required

{path} is the file inside the build, for example assets/remoteEntry.js or assets/index-4f2a.css.

Each of the three forms also accepts a version pinned into the path, immediately before {path}:

GET <API_BASE>/serve/mfe/files/auto/{projectId}/{mfeSlug}/_v/{version}/{path}

The platform resolves the version — from the pinned segment when present, otherwise from the deployment this URL form answers from, which is not always the active one, and any canary configuration — and fetches the bytes according to the microfrontend's hosting type — local disk, your bucket, or an external URL — then streams them back. A pinned version is honoured only if that deployment can serve it: the deployed version, or the configured canary version.

The entry point redirect

When a microfrontend is running a version-based canary and its entry point is requested at a versionless URL, the response is a 302 (uncacheable) to the same file under /_v/<resolved-version>/. This exists as a fallback for callers holding a raw URL; the URLs handed out by the manifest already carry the segment, so they never take the redirect.

The redirect is sufficient for an ES module, whose relative imports resolve against the URL after redirects. It is not sufficient for a classic script: document.currentScript.src is the URL before redirects, so a webpack build with publicPath: 'auto' would derive its chunk base from the versionless URL and could mix two versions in one page. Take the manifest URL as it is and the question does not arise.

:::caution The Location is root-relative and carries no /api prefix The redirect points at /serve/mfe/files/…, not at <API_BASE>/serve/mfe/files/…. Behind the shipped nginx, which proxies the backend under location /api/ and serves the console SPA from /, the browser is therefore sent to the SPA instead of to the file. The fallback only works when the backend is reachable at the origin root — running standalone, or behind a proxy that mounts it there. One more reason to use the manifest URL as it is: it already carries the version and never redirects. :::

Version override

GET <API_BASE>/serve/all/{projectId}/{environmentSlug}?mfeVersion=1.5.0-rc1
GET <API_BASE>/serve/mfe/files/auto/{projectId}/{mfeSlug}/{path}?mfeVersion=1.5.0-rc1

mfeVersion forces a specific version, which is how you look at a canary without waiting to be drawn into it. It is accepted only when the value is one of the versions that deployment can serve, so it cannot reach an arbitrary build.

Headers

SituationHeaders
The file is the microfrontend's entry pointCache-Control: no-cache, no-store, must-revalidate, Pragma: no-cache, Expires: 0
Any other fileCacheable
All filesCross-Origin-Resource-Policy: cross-origin, x-mfe-version: <version>

Content-Type is set from the extension for .js, .css, .html and .xml.

x-mfe-version names the version those exact bytes came from. It is the only thing a browser is ever told about a canary, and the quickest way to check which side of a split a page landed on.

The entry point being uncacheable is what makes a deployment take effect promptly: browsers re-fetch it, discover the new hashed chunk names inside, and load those from cache or network as needed.

Generated bundler configuration

GET <API_BASE>/serve/code?microfrontendId={id}&deploymentId={id}
&framework={react|vue|angular}&compiler={vite|webpack|webcomponent}

framework and compiler are the two axes of the stack, and both are optional: left out, each falls back to the stack stored on the microfrontend, which is the normal case. They exist so the console can ask for another stack — or for one at all, when detection found none.

:::caution They are two different parameters vite and webpack are compiler values, not framework values. framework=vite parses to undefined, the request then has no framework to generate for, and the response comes back with an empty code and no config — a silent empty answer rather than an error. :::

The response is the whole set of instructions, not a single string:

FieldContents
stack{ framework, compiler, source } — the stack the instructions were generated for, and where it came from (TEMPLATE, DETECTED, MANUAL)
configPathWhere the config belongs in the repository, ex. vite.config.js
configFull content of the bundler config
bootstrapThe commented-out configure() snippet for the entry point
codeconfig and bootstrap concatenated — the text the Integration page displays
dependenciesPackages the config needs beyond what the app already has
installCommandnpm install …, absent when dependencies is empty
runtimeIntegrationtrue for a Web Component microfrontend: there is no federation config, the host resolves the URL at runtime

stack is always present. A Web Component stack answers with runtimeIntegration: true, an empty code and nothing else; a microfrontend whose stack could not be determined answers with an empty code and no config.

The console's CURL tab shows the runtime-discovery call for the selected environment, with your ids already substituted:

The CURL tab of the Integration page

Unlike the rest of this page, this endpoint requires authentication. It is a convenience for tooling, not something your application calls at runtime.

Error responses

Branch on the code field, not on the message. The messages are written for a human reading a log and get reworded as they get more helpful; code is the discriminator.

codeStatusMessageCause
ENTITY_NOT_FOUND404Entity not found with id Active deploymentThe environment has never been deployed, or has no active deployment
ENTITY_NOT_FOUND404Entity not found with id <slug or id>No such microfrontend, or the microfrontend is absent from the deployment that answered
ENVIRONMENT_NOT_FOUND404No environment of project <id> has "<domain>" among its registered domains, …An auto form was called from a domain registered on no environment of that project
ENVIRONMENT_NOT_FOUND404Environment not found: <slug or id>An explicit slug or id form named an environment that does not exist
PROJECT_NOT_FOUND404Project not found: <id>The project id in the path is not a project of this console
none500Referer not foundA Referer-required endpoint was called with neither a Referer nor a Host header

Note the shape of the first message: Active deployment is the entity id slotted into a generic template, which is why it reads the way it does. There is no Active deployment not found message.

Referer not found is the odd one out. It is thrown as a plain error with no status attached, so it surfaces as a 500 rather than as the 4xx its neighbours return, and it carries no code. Only the two /serve/mfe/…/{mfeId}/… forms demand a Referer outright; the auto forms fall back to the Host header first, so in practice only a request carrying neither reaches this.

A microfrontend that exists in the project but was added after the last deployment falls into the second row: deploy the environment to include it.

Building runtime discovery

Use the client SDK. It calls this endpoint for you, once per page load, with the identity parameters generated and persisted, and returns URLs you can hand straight to your federation runtime:

// src/main.js
import { configure, remoteUrl, globalVariables } from '@mfe-orchestrator-hub/client'

configure({ backendUrl: API_BASE, projectId: PROJECT_ID, environment: ENV_SLUG })
// environment is optional: drop it and the SDK calls the auto form instead,
// letting the domain decide which environment answers.

window.globalConfig = await globalVariables()

const catalog = await remoteUrl('catalog') // already version-pinned, use as is

await import('./App')

The generated bundler configuration already goes through remoteUrl(), so in most hosts there is nothing to write beyond the configure() call.

If you do call the endpoint yourself, remember what the SDK was written to get right: generate and persist a session id and a device id, send them on every manifest request, and pass each url through untouched.

With this in place, adding a remote to the host is a deployment rather than a release: the roster is discovered, not compiled in.