Skip to content

HTTP API Reference

This page is the complete reference for the Remote Access HTTP API exposed by Halo Server (the headless server edition). Third-party business systems (for example a Java backend powering a customer-service "digital employee") integrate Halo as an AI agent backend through this API.

Transport model (important): this API is asynchronous and event-driven by design. There is no synchronous "ask → get answer" endpoint. When you send a message to an agent / digital human, the endpoint returns an acknowledgement (ack) immediately; the AI's thoughts and final result are delivered over WebSocket events, or fetched by polling the status endpoints. See WebSocket Event Protocol.

All paths are relative to the deployment root. If the service is mounted under a reverse-proxy path prefix (e.g. /fc-xxxx/), prepend that prefix to every path below — e.g. https://host/fc-xxxx/api/apps.


Authentication

What the credential is

The caller credential is Halo's remote-access service token (the "remote access password"). It is carried as a standard Bearer token on every API request:

Authorization: Bearer <token>

The token is held by the server (stored in config remoteAccess.password). It is either a 12-character strong random string generated when remote access is first enabled, or a custom password set by the user. In a Server (headless) deployment, it is conventionally supplied via the HALO_REMOTE_PASSWORD environment variable (see Configuration).

The Bearer token works without calling the login endpoint first. The login endpoint (/api/remote/login) exists only for the browser UI login flow. Machine-to-machine integrations simply send Authorization: Bearer <token> on every request.

Auth is enforced by the /api/* middleware. The following paths are public (no token required):

  • POST /api/remote/login
  • GET /api/remote/status
  • GET /api/security/policy

Every other /api/* endpoint returns 401 without a valid token.


POST /api/remote/login

Validate an access token. Used only by the browser UI login flow; machine integrations can skip this and use the Bearer header directly.

FieldTypeRequiredDescription
tokenstringyesThe service token

Response

  • 200{ "success": true }
  • 401{ "success": false, "error": "Invalid token" }
  • 429{ "success": false, "error": "Too many failed attempts. Try again later.", "code": "LOCKED" }, with a Retry-After header (seconds)

The login endpoint applies per-source-IP rate limiting and lockout: repeated failures trigger a temporary lock during which 429 is returned.

bash
curl -X POST https://host/api/remote/login \
  -H "Content-Type: application/json" \
  -d '{"token":"<token>"}'

GET /api/remote/status

Public health probe, no auth required.

Response

json
{
  "success": true,
  "data": { "active": true, "clients": 0, "version": "1.0.0" }
}
  • clients: current number of WebSocket connections.
  • version: the remote-access protocol version (fixed 1.0.0; this is NOT the app version — see GET /api/system/version).
bash
curl https://host/api/remote/status

Digital-human (App) lifecycle and chat

A "digital human" is internally an App (automation app / agent). This is the commercial core: each digital human has its own persona (system prompt), config, memory, and conversations.

Common response convention

Unless noted otherwise, endpoints return { "success": true, "data": ... } or { "success": false, "error": "..." }.

App services initialize lazily. Early in process startup, if the App Manager / Runtime is not yet ready, the relevant endpoints return 503:

json
{ "success": false, "error": "App Manager is not yet initialized. Please try again shortly." }

GET /api/apps

List all installed Apps.

Query parameters

ParamTypeRequiredDescription
spaceIdstringnoFilter by space
statusstringnoFilter by status (e.g. active, paused). When omitted, uninstalled apps are excluded

Response: data is an array of App objects.

bash
curl https://host/api/apps \
  -H "Authorization: Bearer <token>"

POST /api/apps/install

Install an App (digital human / automation app).

FieldTypeRequiredDescription
specobjectyesApp spec object (AppSpec)
spaceIdstring | nullnoTarget space. null = global install (MCP/Skill available across spaces)
userConfigobjectnoUser config (matching the spec's config_schema)

Response: on success { "success": true, "data": { "appId": "..." } }.

Status codes

  • 400: missing spec or invalid spaceId type
  • 403: remote install of MCP-command apps is blocked (code: "MCP_COMMAND_BLOCKED")
  • 409: already installed (code: "ALREADY_INSTALLED")
  • 503: not initialized (code: "NOT_INITIALIZED")

HTTP install semantics: only automation-type Apps are activated after install (activateNonAutomation: false).

bash
curl -X POST https://host/api/apps/install \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId>","spec":{ /* AppSpec */ },"userConfig":{}}'

GET /api/apps/:appId

Get a single App.

bash
curl https://host/api/apps/<appId> \
  -H "Authorization: Bearer <token>"

DELETE /api/apps/:appId

Uninstall (soft-delete) an App. Deactivates it in the Runtime first, then marks it uninstalled.

Query parameters

ParamTypeRequiredDescription
purgestringnotrue to also purge data
bash
curl -X DELETE "https://host/api/apps/<appId>?purge=true" \
  -H "Authorization: Bearer <token>"

POST /api/apps/:appId/config

Update an App's user config (matching the spec's config_schema). The request body is the config object (full replacement).

bash
curl -X POST https://host/api/apps/<appId>/config \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"someKey":"someValue"}'

POST /api/apps/:appId/trigger

Manually trigger one run (for automation-type Apps). Async: returns a trigger result summary in data (including outcome); the actual execution surfaces via Activity / WebSocket.

bash
curl -X POST https://host/api/apps/<appId>/trigger \
  -H "Authorization: Bearer <token>"

App chat (core)

Digital-human chat is the core interaction for commercial integration. Chat is asynchronous and streaming: chat/send returns an ack immediately, the AI's thoughts and reply are pushed over WebSocket (subscribe to the matching conversationId), and messages are persisted for replay.

Each digital human has a default conversation whose conversationId is app-chat:{appId}.

POST /api/apps/:appId/chat/send

Send a message to a digital human's AI agent. Async: returns immediately, generation runs in the background, results stream over WebSocket.

The request body is spread through to the underlying layer ({ ...req.body, appId }), so the following fields are supported:

FieldTypeRequiredDescription
spaceIdstringyesThe space the App lives in (required by the underlying executor)
messagestringyesUser message text
imagesarraynoMultimodal image attachments
thinkingEnabledbooleannoEnable extended thinking
conversationIdstringnoCustom conversation ID for an isolated, per-end-user conversation under the same digital-human persona (see note below)

Response

json
{ "success": true, "data": { "conversationId": "app-chat:<appId>" } }

About conversationId isolation (important detail): The underlying sendAppChatMessage uses request.conversationId ?? "app-chat:{appId}" as the conversation/session-isolation key. So you can pass a distinct conversationId per end-user (e.g. app-chat:<appId>:user-123) to get isolated conversation context and history under the same digital-human persona. Note: this endpoint's response body always echoes the default app-chat:{appId}, and does not echo your custom value. To subscribe to WebSocket events for a custom conversation, use the conversationId you passed in — not the one returned in the response.

bash
curl -X POST https://host/api/apps/<appId>/chat/send \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId>","message":"Hello","conversationId":"app-chat:<appId>:user-123"}'

GET /api/apps/:appId/chat/status

Get the digital human's current chat status.

Response

json
{ "success": true, "data": { "isGenerating": false, "conversationId": "app-chat:<appId>" } }

conversationId is always the default value; isGenerating reflects whether any session of this digital human (including custom and IM sessions) is currently generating.

bash
curl https://host/api/apps/<appId>/chat/status \
  -H "Authorization: Bearer <token>"

POST /api/apps/:appId/chat/stop

Stop all active chat generations for this digital human (default + custom/IM sessions). Returns { "success": true }.

bash
curl -X POST https://host/api/apps/<appId>/chat/stop \
  -H "Authorization: Bearer <token>"

GET /api/apps/:appId/chat/messages

Load the persisted message history of the digital human's default conversation.

BehaviorDescription
App not found404
No space pathreturns { "success": true, "data": [] }

Only the default conversation (app-chat:{appId}, storage runId chat) messages are returned. History of custom conversationIds is not included here.

bash
curl https://host/api/apps/<appId>/chat/messages \
  -H "Authorization: Bearer <token>"

POST /api/apps/:appId/chat/clear

Clear the digital human's default conversation history and reset to a fresh session.

FieldTypeRequiredDescription
spaceIdstringyesUsed to resolve the storage path (body)
bash
curl -X POST https://host/api/apps/<appId>/chat/clear \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId>"}'

POST /api/apps/:appId/chat/restart

Restart all chat sessions for this digital human — closes the underlying subprocesses so the next message reloads the system prompt and config. Conversation history is preserved (restored via the saved sessionId).

Response: { "success": true, "data": { "sessionsClosed": <number> } }

bash
curl -X POST https://host/api/apps/<appId>/chat/restart \
  -H "Authorization: Bearer <token>"

GET /api/apps/:appId/chat/session-state

Get the default conversation's session state (for recovery after refresh).

Response

json
{ "success": true, "data": { "isActive": false, "thoughts": [], "spaceId": "<spaceId>" } }

App state and activity (automation runs)

GET /api/apps/:appId/state

Get the real-time automation App state (AutomationAppState).

bash
curl https://host/api/apps/<appId>/state \
  -H "Authorization: Bearer <token>"

GET /api/apps/:appId/activity

Get the App's activity entries.

ParamTypeRequiredDescription
limitnumbernoMax number of entries
beforenumbernoTimestamp cursor (before this)
bash
curl "https://host/api/apps/<appId>/activity?limit=50" \
  -H "Authorization: Bearer <token>"

POST /api/apps/:appId/escalation/:entryId/respond

Respond to an escalation request.

FieldTypeRequiredDescription
choicestringnoOption identifier
textstringnoFree-text reply
bash
curl -X POST https://host/api/apps/<appId>/escalation/<entryId>/respond \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"choice":"approve"}'

Other App management endpoints

The following endpoints also exist (all require Bearer auth) for fuller lifecycle management:

Method + PathPurpose
POST /api/apps/:appId/reinstallReinstall a previously uninstalled App
DELETE /api/apps/:appId/permanentPermanently delete an App and all its data (built-in apps protected)
POST /api/apps/:appId/move-spaceMove an App to a different space (or global)
POST /api/apps/:appId/clear-memoryDelete all memory files for an App
POST /api/apps/:appId/pausePause an App
POST /api/apps/:appId/resumeResume an App
POST /api/apps/:appId/frequencyUpdate subscription frequency (subscriptionId, frequency)
PATCH /api/apps/:appId/specUpdate spec (JSON Merge Patch)
PATCH /api/apps/:appId/overridesUpdate user overrides (JSON Merge Patch, null deletes a key)
POST /api/apps/:appId/permissions/grantGrant a permission
POST /api/apps/:appId/permissions/revokeRevoke a permission
POST /api/apps/:appId/upgrade-strategySet upgrade strategy (auto/notify/manual)
GET /api/apps/:appId/export-specExport spec as YAML
POST /api/apps/import-specInstall from YAML
POST /api/apps/:appId/runs/:runId/continueContinue a run that failed on premature stop
POST /api/apps/:appId/runs/:runId/injectInject user text into an active run (text)
GET /api/apps/:appId/runs/:runId/sessionRead a run's session messages ("View process")
GET /api/apps/:appId/chat/session-stateDefault conversation session state
GET /api/apps/:appId/im-chat/messagesRead IM channel session messages
POST /api/apps/:appId/im-chat/clearClear an IM session

Generic agent and conversations

Besides "digital humans", Halo Server also exposes a generic agent interface that drives a conversation directly within a space. It is likewise asynchronous and streaming: after sending a message, subscribe to the conversation's WebSocket events.

POST /api/agent/message

Send a message to the generic agent. Async: returns { "success": true } as an ack; the AI output streams over WebSocket to that conversationId.

FieldTypeRequiredDescription
spaceIdstringyesSpace ID
conversationIdstringyesConversation ID (use the same one to subscribe to WS events)
messagestringyesUser message
resumeSessionIdstringnoResume a specific SDK session
imagesarraynoMultimodal images
thinkingEnabledbooleannoExtended thinking
aiBrowserEnabledbooleannoEnable AI Browser tools
bash
curl -X POST https://host/api/agent/message \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId>","conversationId":"<convId>","message":"Hello"}'

POST /api/agent/stop

Stop generation for a conversation (or all).

FieldTypeRequiredDescription
conversationIdstringnoConversation ID

POST /api/agent/approve · POST /api/agent/reject

Approve / reject a tool call.

FieldTypeRequiredDescription
conversationIdstringyesConversation ID

In the current implementation, permissions are auto-allowed by default, so these two endpoints are no-ops and always return { "success": true }.

POST /api/agent/answer-question

Answer an AskUserQuestion raised by the agent.

FieldTypeRequiredDescription
conversationIdstringyesConversation ID
idstringyesQuestion ID
answersobjectyesAnswer map (field → value)

If no pending question matches that id, returns { "success": false, "error": "No pending question found for id: ..." }.

GET /api/agent/session/:conversationId

Get conversation state (for recovery after refresh). Returns { "success": true, "data": <SessionState> }.

GET /api/agent/generating/:conversationId

Check whether a conversation is currently generating. Returns { "success": true, "data": <boolean> }.

bash
curl https://host/api/agent/generating/<convId> \
  -H "Authorization: Bearer <token>"

GET /api/agent/sessions

List the conversationIds of all active sessions. Returns { "success": true, "data": ["<convId>", ...] }.

Other agent endpoints

Method + PathPurpose
POST /api/agent/test-mcpTest MCP server connections
GET /api/agent/engine-capabilitiesQuery current engine capabilities

Spaces and conversation storage

A conversation's persisted record is managed via space-scoped endpoints. These are synchronous read/write endpoints (they operate on storage directly and do not trigger AI generation).

Method + PathPurpose
GET /api/spacesList all spaces
POST /api/spacesCreate a space (name/icon/customPath)
GET /api/spaces/:spaceIdGet a space
PUT /api/spaces/:spaceIdUpdate a space
DELETE /api/spaces/:spaceIdDelete a space
GET /api/spaces/default-pathGet the default spaces dir
GET /api/spaces/haloGet the Halo temp space
GET /api/spaces/:spaceId/conversationsList conversations
POST /api/spaces/:spaceId/conversationsCreate a conversation (title)
GET /api/spaces/:spaceId/conversations/:conversationIdGet a conversation
PUT /api/spaces/:spaceId/conversations/:conversationIdUpdate a conversation
DELETE /api/spaces/:spaceId/conversations/:conversationIdDelete a conversation
POST /api/spaces/:spaceId/conversations/:conversationId/messagesAppend a message
PUT /api/spaces/:spaceId/conversations/:conversationId/messages/lastUpdate the last message
GET /api/spaces/:spaceId/conversations/:conversationId/messages/:messageId/thoughtsGet a message's thoughts
POST /api/spaces/:spaceId/conversations/:conversationId/starToggle star (starred)
bash
# Create a conversation
curl -X POST https://host/api/spaces/<spaceId>/conversations \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"title":"New conversation"}'

Integration tip: POST /api/agent/message only drives AI generation and streams over WebSocket; it does not create a conversation record for you. The typical flow is: POST .../conversations to create a conversation and obtain a conversationId, subscribe over WebSocket, then POST /api/agent/message to send the message.


Config and health

GET /api/config

Get the service config. Returns { "success": true, "data": <Config> }.

bash
curl https://host/api/config \
  -H "Authorization: Bearer <token>"

POST /api/config

Write the service config (the request body is the config object).

Remote calls are policy-gated: changes touching MCP commands or the browser allowlist may be rejected (403, code: "MCP_COMMAND_BLOCKED" etc.).

Other config endpoints

Method + PathPurpose
POST /api/config/validateValidate API credentials (apiKey/apiUrl/provider/model)
POST /api/config/fetch-modelsFetch available model list (apiKey/apiUrl)
POST /api/config/refresh-ai-sourcesRefresh all AI source configs
GET /api/security/policyPublic security-policy slice (no auth)

GET /api/system/version

Get the app version.

Response

json
{ "success": true, "data": "x.y.z" }
bash
curl https://host/api/system/version \
  -H "Authorization: Bearer <token>"

Other system endpoints

Method + PathPurpose
GET /api/auth/providersList enabled auth providers (read-only)
POST /api/analytics/reportReport a telemetry event (event/properties, fire-and-forget)

Errors and status-code convention

  • Business failures typically return HTTP 200 with { "success": false, "error": "..." } (many handlers res.json directly in their catch block without changing the status code).
  • Auth failure: 401.
  • Rate-limit lockout: 429 (with Retry-After).
  • Service not ready: 503.
  • Install-related semantic statuses: 400 (params), 403 (MCP blocked), 409 (already installed), 422 (validation failed), 404 (not found).

When integrating, always check both the HTTP status code and the success field in the response body.