Download OpenAPI specification:
The HTTP surface of Mercury — the intended primary channel for a custom web UI. It is enabled by declaring the http channel in mercury.config.ts, together with an auth provider (auth): without one the surface doesn't start. CORS is enabled on every response (Access-Control-Allow-Origin, configurable via HTTP_SURFACE_CORS_ORIGIN, default *) and every route answers an OPTIONS preflight, so a browser UI on another origin can call it.
Every route except this document, /version and the OPTIONS preflights needs Authorization: Bearer <token>. The app's auth provider decides what a valid token is (an OpenID Connect access token, a static test token); a missing or refused one gets 401 with WWW-Authenticate: Bearer, before anything runs.
Then the core decides whether it talks to that person at all: someone the app's directory doesn't know gets 403 (unless the app lets unknown people in), and 503 when the directory can't be reached. Both carry { ok: false, error, reason }, error being what to tell them.
Every read is scoped to the caller: their own conversations, pending confirmations, tool calls and memory, and the wiki as they see it, the team's common area (curated/) plus their own notes (personal/). Someone else's data doesn't exist as far as a caller can tell. /manifest lists what the caller is offered, and only /health is the same for everyone.
A conversation is identified by an opaque, client-owned conversationId and belongs to whoever opened it: Mercury's session key is <caller id>:<conversationId>, so the same id sent by someone else is a conversation of their own, and a confirmation token staged in one person's conversation is never found from another's. Omitting conversationId on POST /turn starts a fresh ephemeral conversation. The durable transcript of one of the caller's conversations is readable via GET /conversation, and the list of their conversations via GET /conversations (both backed by the verbatim archive; they degrade to empty when the vector store is unreachable).
A conversation never ends: sent again with the same conversationId, a turn continues it however long after. Mercury frees a conversation's in-memory history once it has been idle a while; the next turn rebuilds it from the archive (a summary of its older part and its newest messages, within HTTP_SURFACE_RESUME_MAX_CHARS), so the model still knows what the person sees on screen.
POST /turn streams the answer as Server-Sent Events: reasoning and text arrive as incremental deltas (never a single finished block), followed by an authoritative final event. To cancel a turn whose generation is diverging, the client simply closes the connection — Mercury aborts the in-flight generation.
An irreversible action is never executed on the model's say-so: the turn emits a pending event carrying a one-time token. The client confirms by sending that token back — either as the text of a follow-up POST /turn, or via POST /confirm. An action can offer several choices (e.g. "only this time" / "always"), each with its own token: the caller sends back one, and the others die with it.
Some actions hand their result back to the model instead of to the caller (a page read after the caller allowed it, for example). Confirming one by POST /turn streams the model's answer on that same stream; POST /confirm answers resume: true, and the client then opens POST /turn { resume: true, conversationId } to hear it. The answer waits five minutes and is heard once: later, after a restart, or a second time, that final's text says it's no longer available.
Runs one model turn and streams the result as text/event-stream. If text is a pending confirmation token it is resolved deterministically without ever consulting the model. Close the connection to cancel an in-flight turn. When the confirmation hands its result back to the model, the model's answer to it streams instead. With resume: true and no text, the turn is the one a /confirm answered resume: true for. With opening: true and no text, the assistant speaks first in the conversation named (right after someone signs in): it does only for someone it doesn't know the name of, asking how to call them, and otherwise the stream carries a single final with an empty text. Every text the person reads that isn't the model's (a refusal, a status line, a confirmation's reply) is in their language: the one they saved, else the one Accept-Language asks for, else the instance's default. The model reads the person's message after when and where they wrote it: in the time zone they saved, else the one X-Time-Zone says, else the instance's.
| Accept-Language | string The languages the client prefers, as browsers send them ( |
| X-Time-Zone | string The time zone the client is in, by its IANA name, as a browser gives it ( |
| text | string The user's message, or a bare confirmation token. Required, unless |
| opening | boolean Value: true The assistant speaks first in |
| resume | boolean Value: true The turn a |
| conversationId | string^[A-Za-z0-9_-]{1,128}$ Opaque, client-owned; letters, digits, |
{- "text": "string",
- "opening": true,
- "resume": true,
- "conversationId": "string"
}{- "ok": false,
- "error": "string"
}Runs a one-time confirmation token through the same mechanism the bare-token-in-text path uses, keyed on the caller's own conversation (<caller id>:<conversationId>). Never touches the model. The reply is in the caller's language, as on /turn.
| Accept-Language | string The languages the client prefers, as browsers send them ( |
| token required | string |
| conversationId required | string^[A-Za-z0-9_-]{1,128}$ |
{- "token": "string",
- "conversationId": "string"
}{- "ok": true,
- "resolved": true,
- "text": "string",
- "resume": true
}A one-time code (xxxx-xxxx-xxxx, valid for ten minutes) the person sends from another account, on any channel, twice: the first time Mercury says whom the account would be linked to, the second time it links it. From then on both accounts are one person. Only the caller sees the code; the model never does.
{- "ok": true,
- "code": "string",
- "expiresAt": "2019-08-24T14:15:22Z"
}| id required | string^[A-Za-z0-9_-]{1,128}$ The caller's |
| limit | integer Default: 200 |
| offset | string Opaque pagination cursor from a previous page's |
{- "ok": true,
- "messages": [
- {
- "role": "user",
- "content": "string",
- "timestamp": "string"
}
], - "nextOffset": "string"
}| limit | integer Default: 50 |
{- "ok": true,
- "conversations": [
- {
- "conversationId": "string",
- "lastTimestamp": "string",
- "preview": "string"
}
]
}How the caller asked to be called, null when they never said: a UI asks for an opening turn after a sign-in only then. Also the language they asked the assistant to use and the time zone they saved, null when they never said (missing on a core that doesn't know them). Not there on an older core, which can't say.
{- "ok": true,
- "name": "string",
- "language": "string",
- "timeZone": "string"
}The time zone the caller is in, by its IANA name, for a UI's settings: from then on their turns are stamped with the time there, whatever X-Time-Zone says. Saved as the IANA list writes it (europe/rome is Europe/Rome). Not there on a core without settings.
| timeZone required | string An IANA name such as |
{- "timeZone": "string"
}{- "ok": true,
- "timeZone": "string"
}| collection required | string One of the collections kept per person (the verbatim archive, episodic memory, semantic facts), by its configured name. |
| limit | integer Default: 50 |
| offset | string |
{ }Mounted when HTTP_SURFACE_PUBLIC_URL is set. The service sends the person's browser here with code and state (or error); the single-use state is the credential, so no bearer token is needed. Answers with a plain-text page, in the language the browser asks for (Accept-Language), else the instance's default; a failed login reads in the language of the person who started it.
| code | string |
| state | string |
| error | string |
| Accept-Language | string The languages the client prefers, as browsers send them ( |