---
title: "Digital Humans API: install, configure and chat"
description: "Halo HTTP API for Digital Humans: install and uninstall, pause and resume, configure, read run history and activity, and chat with them."
type: "reference"
audience: ["developer"]
lang: "en"
canonical: "https://hello-halo.cc/docs/en/developers/api/digital-human.html"
last_updated: "2026-10-19T00:11:17+08:00"
source: "Halo 3.0.4 docs"
---
# API: Digital Humans

Halo HTTP API for Digital Humans: install and uninstall, pause and resume, configure, read run history and activity, and chat with them.

Digital Humans (`app` in the API) start work on a schedule, on events, or when someone messages them. Use these operations to install and remove them, pause and resume them, change their configuration, read their run history and activity, and chat with them.

Paths are relative to your Halo base URL, and every request carries the access password. Set `$HALO_URL` and `$HALO_TOKEN` as in the [quickstart](https://hello-halo.cc/docs/en/developers/quickstart.html); base URL, authentication and error conventions are in the [HTTP API overview](https://hello-halo.cc/docs/en/developers/api/).

| Operation | Endpoint |
| --- | --- |
| [List digital humans (and other installed apps) in a space](#get-api-apps) | `GET /api/apps` |
| [Get one installed digital human (or other app) by id](#get-api-apps-appid) | `GET /api/apps/:appId` |
| [Soft-delete (uninstall) a digital human; recoverable via reinstall](#delete-api-apps-appid) | `DELETE /api/apps/:appId` |
| [Get the activity/run-history entries for a digital human](#get-api-apps-appid-activity) | `GET /api/apps/:appId/activity` |
| [List skills this digital human can load (global + space-local)](#get-api-apps-appid-available-skills) | `GET /api/apps/:appId/available-skills` |
| [Clear a digital human's chat history](#post-api-apps-appid-chat-clear)  (Irreversible) | `POST /api/apps/:appId/chat/clear` |
| [Read a digital human chat transcript](#get-api-apps-appid-chat-messages) | `GET /api/apps/:appId/chat/messages` |
| [Restart a digital human's chat agent process (reloads system prompt and config)](#post-api-apps-appid-chat-restart) | `POST /api/apps/:appId/chat/restart` |
| [Send a message to a digital human and let it reply](#post-api-apps-appid-chat-send) | `POST /api/apps/:appId/chat/send` |
| [Check whether a digital human chat is currently generating a reply](#get-api-apps-appid-chat-status) | `GET /api/apps/:appId/chat/status` |
| [Stop a digital human's in-progress chat reply](#post-api-apps-appid-chat-stop) | `POST /api/apps/:appId/chat/stop` |
| [Clear the history of all of a digital human's chats at once](#post-api-apps-appid-chats-clear-all)  (Irreversible) | `POST /api/apps/:appId/chats/clear-all` |
| [Count the conversations clearing all of a digital human's chats would clear](#get-api-apps-appid-chats-clearable) | `GET /api/apps/:appId/chats/clearable` |
| [Delete all of a digital human's memory files](#post-api-apps-appid-clear-memory)  (Irreversible) | `POST /api/apps/:appId/clear-memory` |
| [Replace a digital human's user configuration values](#post-api-apps-appid-config) | `POST /api/apps/:appId/config` |
| [Answer a digital human's pending question (it stopped to ask a human before continuing)](#post-api-apps-appid-escalation-entryid-respond)  (Irreversible) | `POST /api/apps/:appId/escalation/:entryId/respond` |
| [Change how often a digital human runs, without editing the rest of its definition (spec)](#post-api-apps-appid-frequency) | `POST /api/apps/:appId/frequency` |
| [Clear a digital human's chat history in a bound IM chat](#post-api-apps-appid-im-chat-clear)  (Irreversible) | `POST /api/apps/:appId/im-chat/clear` |
| [Read a digital human's transcript in a bound IM chat](#get-api-apps-appid-im-chat-messages) | `GET /api/apps/:appId/im-chat/messages` |
| [Stop a digital human's in-progress reply in a bound IM chat (keeps history)](#post-api-apps-appid-im-chat-stop) | `POST /api/apps/:appId/im-chat/stop` |
| [Read the state of a digital human's memory](#get-api-apps-appid-memory) | `GET /api/apps/:appId/memory` |
| [Consolidate a digital human's memory now](#post-api-apps-appid-memory-consolidate) | `POST /api/apps/:appId/memory/consolidate` |
| [Set which space a digital human starts new work in](#post-api-apps-appid-move-space) | `POST /api/apps/:appId/move-space` |
| [Merge-patch per-installation overrides (notification level, model)](#patch-api-apps-appid-overrides) | `PATCH /api/apps/:appId/overrides` |
| [Pause a digital human (stops scheduling)](#post-api-apps-appid-pause) | `POST /api/apps/:appId/pause` |
| [Permanently delete a digital human and all its data](#delete-api-apps-appid-permanent)  (Irreversible) | `DELETE /api/apps/:appId/permanent` |
| [Grant a named permission to a digital human](#post-api-apps-appid-permissions-grant) | `POST /api/apps/:appId/permissions/grant` |
| [Revoke a named permission from a digital human](#post-api-apps-appid-permissions-revoke) | `POST /api/apps/:appId/permissions/revoke` |
| [Reinstall a previously soft-deleted (uninstalled) digital human](#post-api-apps-appid-reinstall) | `POST /api/apps/:appId/reinstall` |
| [List the reminders a digital human set in its conversations](#get-api-apps-appid-reminders) | `GET /api/apps/:appId/reminders` |
| [Cancel a reminder a digital human set in one of its conversations](#delete-api-apps-appid-reminders-reminderid)  (Irreversible) | `DELETE /api/apps/:appId/reminders/:reminderId` |
| [Resume a paused digital human](#post-api-apps-appid-resume) | `POST /api/apps/:appId/resume` |
| [Resume a run that stopped prematurely on an error](#post-api-apps-appid-runs-runid-continue)  (Irreversible) | `POST /api/apps/:appId/runs/:runId/continue` |
| [Inject a text message into a currently active run](#post-api-apps-appid-runs-runid-inject)  (Irreversible) | `POST /api/apps/:appId/runs/:runId/inject` |
| [Read what a run actually did, step by step](#get-api-apps-appid-runs-runid-session) | `GET /api/apps/:appId/runs/:runId/session` |
| [Start a new chat thread with a digital human](#post-api-apps-appid-sessions-create) | `POST /api/apps/:appId/sessions/create` |
| [Delete a chat thread with a digital human](#post-api-apps-appid-sessions-delete)  (Irreversible) | `POST /api/apps/:appId/sessions/delete` |
| [Branch an existing chat thread into a new one, copying its history](#post-api-apps-appid-sessions-fork) | `POST /api/apps/:appId/sessions/fork` |
| [Edit part of a digital human's definition (spec) without replacing all of it](#patch-api-apps-appid-spec) | `PATCH /api/apps/:appId/spec` |
| [Get real-time run state of a digital human (running/queued/idle/etc.)](#get-api-apps-appid-state) | `GET /api/apps/:appId/state` |
| [Manually trigger a digital human to run immediately](#post-api-apps-appid-trigger) | `POST /api/apps/:appId/trigger` |
| [Set how a store-installed digital human handles version upgrades](#post-api-apps-appid-upgrade-strategy) | `POST /api/apps/:appId/upgrade-strategy` |
| [Install a digital human (or other app type) from a YAML definition (spec) string](#post-api-apps-import-spec) | `POST /api/apps/import-spec` |
| [Install a digital human (or other app type) from its full definition (spec) object](#post-api-apps-install) | `POST /api/apps/install` |
| [List every chat thread a digital human has, its own and IM ones](https://hello-halo.cc/docs/en/developers/api/channels.html#get-api-im-sessions) (Message channels) | `GET /api/im-sessions` |
| [List the IM chats a digital human pushes to through another digital human's bot](https://hello-halo.cc/docs/en/developers/api/channels.html#get-api-im-sessions-linked) (Message channels) | `GET /api/im-sessions/linked` |
| [Remove a chat binding from the IM session registry](https://hello-halo.cc/docs/en/developers/api/channels.html#post-api-im-sessions-remove) (Message channels) | `POST /api/im-sessions/remove` |
| [Set a custom display name for a chat binding](https://hello-halo.cc/docs/en/developers/api/channels.html#post-api-im-sessions-set-custom-name) (Message channels) | `POST /api/im-sessions/set-custom-name` |
| [Toggle whether a run's result is auto-pushed to this chat when it finishes](https://hello-halo.cc/docs/en/developers/api/channels.html#post-api-im-sessions-set-proactive) (Message channels) | `POST /api/im-sessions/set-proactive` |
| [Let a digital human push to an IM chat another digital human's bot knows, or stop it](https://hello-halo.cc/docs/en/developers/api/channels.html#post-api-im-sessions-set-push-link) (Message channels) | `POST /api/im-sessions/set-push-link` |
| [Bind a knowledge collection so a digital human can read it](https://hello-halo.cc/docs/en/developers/api/knowledge-base.html#post-api-tlon-kbid-bind-app) (Knowledge bases) | `POST /api/tlon/:kbId/bind-app` |
| [Unbind a knowledge collection from a digital human](https://hello-halo.cc/docs/en/developers/api/knowledge-base.html#post-api-tlon-kbid-unbind-app) (Knowledge bases) | `POST /api/tlon/:kbId/unbind-app` |

## List digital humans (and other installed apps) in a space {#get-api-apps}

`GET /api/apps`  (Assistant)

- Optional query parameters: `spaceId` (only that workspace) and `status`. Without `status`, uninstalled Digital Humans are left out. Without `spaceId`, the list covers every workspace, as the Halo app shows it.
- `userConfig` and the `env` and `headers` of an MCP app's `spec.mcp_server` can hold credentials in plain text, and this API returns them as stored. Treat the response like a secret.

**Query parameters**: `spaceId`, `status`

**Response**

```ts
{success:true,data:[{id,specId,spaceId,spec,status,userConfig,userOverrides,permissions,installedAt,lastRunAt?,lastRunOutcome?,errorMessage?}]}
```

**Example**

```bash
curl -s "$HALO_URL/api/apps" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Get one installed digital human (or other app) by id {#get-api-apps-appid}

`GET /api/apps/:appId`  (Assistant)

- `userConfig` and the `env` and `headers` of an MCP app's `spec.mcp_server` can hold credentials in plain text, and this API returns them as stored. Treat the response like a secret.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:InstalledApp|null}  // same shape as the entries of GET /api/apps
```

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Soft-delete (uninstall) a digital human; recoverable via reinstall {#delete-api-apps-appid}

`DELETE /api/apps/:appId`  (Reversible)  (Assistant)

- Uninstalls the Digital Human but keeps its data, so [`POST /api/apps/:appId/reinstall`](#post-api-apps-appid-reinstall) brings it back. The `purge=true` query parameter is accepted but currently has no effect.
- To delete it for good, call [`DELETE /api/apps/:appId/permanent`](#delete-api-apps-appid-permanent) afterwards.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Query parameters**: `purge`

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX DELETE "$HALO_URL/api/apps/<appId>" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Get the activity/run-history entries for a digital human {#get-api-apps-appid-activity}

`GET /api/apps/:appId/activity`  (Assistant)

- Optional query parameters: `limit` (most entries), `before` (only entries older than this time, epoch ms), `beforeId`, `offset`, `type` (`run_complete`, `run_skipped`, `run_error`, `milestone`, `escalation` or `output`), and `teamId` with `epochId` (entries of one team run).

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Query parameters**: `before`, `beforeId`, `epochId`, `limit`, `offset`, `teamId`, `type`

**Response**

```ts
{success:true,data:[{id,appId,runId,type:"run_complete"|"run_skipped"|"run_error"|"milestone"|"escalation"|"output",ts,content,userResponse?}]}
```

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/activity" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## List skills this digital human can load (global + space-local) {#get-api-apps-appid-available-skills}

`GET /api/apps/:appId/available-skills`  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:[{name,description,scope:"global"|"space",dirName,path}]}
```

**Errors**

- 404-equivalent = {success:false,error:"App not found or has no space"} — the app must have a spaceId

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/available-skills" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Clear a digital human's chat history {#post-api-apps-appid-chat-clear}

`POST /api/apps/:appId/chat/clear`  (Irreversible)  (Assistant)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
:::

- spaceId is required even though the app is already scoped to one. Optional conversationId clears one specific session instead of the default one.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "spaceId": "<spaceId — the app's own spaceId from GET /api/apps>"
}
```

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/chat/clear" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId — the app'\''s own spaceId from GET /api/apps>"}'
```

## Read a digital human chat transcript {#get-api-apps-appid-chat-messages}

`GET /api/apps/:appId/chat/messages`  (Assistant)

- Reads the main thread by default. For another thread pass `?conversationId=`, the same key [`POST /api/apps/:appId/chat/send`](#post-api-apps-appid-chat-send) takes.
- An empty list means the thread has no history yet, or the id is wrong — the two look the same. List the threads with [`GET /api/im-sessions`](https://hello-halo.cc/docs/en/developers/api/channels.html#get-api-im-sessions) rather than guessing.
- `thoughts` holds the raw tool calls and results behind each reply and is large.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Query parameters**: `conversationId`

**Response**

```ts
{success:true,data:[{id,role:"user"|"assistant",content,timestamp,thoughts?,thoughtsSummary?,images?}]}
```

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/chat/messages" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Restart a digital human's chat agent process (reloads system prompt and config) {#post-api-apps-appid-chat-restart}

`POST /api/apps/:appId/chat/restart`  (Assistant)

- Interrupts any in-flight turn. Conversation history is preserved.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{sessionsClosed:number}}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/chat/restart" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Send a message to a digital human and let it reply {#post-api-apps-appid-chat-send}

`POST /api/apps/:appId/chat/send`  (Assistant)

- `spaceId` is the Digital Human's own workspace: the `spaceId` of its entry in [`GET /api/apps`](#get-api-apps).
- `conversationId` picks the thread. Leave it out for the main thread. For a new thread, use the `conversationId` that [`POST /api/apps/:appId/sessions/create`](#post-api-apps-appid-sessions-create) returns. To continue a thread the user started in Halo, build it from [`GET /api/im-sessions`](https://hello-halo.cc/docs/en/developers/api/channels.html#get-api-im-sessions) as `app-chat:&lt;appId&gt;:&lt;channel&gt;:&lt;chatType&gt;:&lt;chatId&gt;`.
- Only threads on the `local` and `http` channels are accepted. A thread that belongs to an IM chat is refused, so this API never speaks into a real conversation with a person. A malformed id returns 400 with the expected form; `chatId` may contain only letters, digits, `_` and `-`.
- Returns before the Digital Human has read the message. Follow the reply over the WebSocket, or poll [`GET /api/apps/:appId/chat/status`](#get-api-apps-appid-chat-status) until `isGenerating` is false and then read [`GET /api/apps/:appId/chat/messages`](#get-api-apps-appid-chat-messages), passing the same `conversationId` to both.
- A message sent while the same thread is still answering doesn't start a second session: it joins the running turn after its current step, or is merged into the next one. Send one message at a time and wait for the reply, so each reply answers one message.
- Optional body fields: `thinkingEnabled`, `reasoningEffort` (`off` to `max`; overrides `thinkingEnabled`, but the Digital Human's own setting wins), `images`.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "spaceId": "<spaceId>",
  "message": "What did you find today?"
}
```

**Response**

```ts
{success:true,data:{conversationId}}  // accepted, not answered
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/chat/send" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId>","message":"What did you find today?"}'
```

## Check whether a digital human chat is currently generating a reply {#get-api-apps-appid-chat-status}

`GET /api/apps/:appId/chat/status`  (Assistant)

- Checks the main thread by default; pass `?conversationId=` for another thread, the same key [`POST /api/apps/:appId/chat/send`](#post-api-apps-appid-chat-send) takes.
- Poll with a growing interval (2 s, then double) instead of a fixed wait.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Query parameters**: `conversationId`

**Response**

```ts
{success:true,data:{isGenerating:boolean,conversationId}}
```

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/chat/status" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Stop a digital human's in-progress chat reply {#post-api-apps-appid-chat-stop}

`POST /api/apps/:appId/chat/stop`  (Assistant)

- Name the thread to stop with `conversationId`. To stop every thread of this Digital Human, send `{"all": true}` instead. Sending neither returns 400.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "conversationId": "<conversationId>"
}
```

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/chat/stop" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"conversationId":"<conversationId>"}'
```

## Clear the history of all of a digital human's chats at once {#post-api-apps-appid-chats-clear-all}

`POST /api/apps/:appId/chats/clear-all`  (Irreversible)  (Assistant)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
:::

- Each chat is cleared as /clear clears it and starts afresh with its next message; replies in progress stop. API sessions, team chats, memory and reminders are left alone. Nothing is posted into the chats.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{cleared:number,failed:number}}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/chats/clear-all" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Count the conversations clearing all of a digital human's chats would clear {#get-api-apps-appid-chats-clearable}

`GET /api/apps/:appId/chats/clearable`  (Assistant)

- total: its default and local chats and its IM chats that have history (im of them in IM); API sessions and team chats are not counted. teamIm: IM chats with history that a team answers — a clear-all leaves them alone; /clear in each clears it.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{total:number,im:number,teamIm:number}}
```

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/chats/clearable" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Delete all of a digital human's memory files {#post-api-apps-appid-clear-memory}

`POST /api/apps/:appId/clear-memory`  (Irreversible)  (Assistant)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
:::

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{filesRemoved:number}}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/clear-memory" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Replace a digital human's user configuration values {#post-api-apps-appid-config}

`POST /api/apps/:appId/config`  (Reversible)  (Assistant)

- Body is the full config object, keyed by config_schema field id. This replaces userConfig wholesale; read current values first with GET /:appId to avoid dropping fields.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "someField": "value"
}
```

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/config" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"someField":"value"}'
```

## Answer a digital human's pending question (it stopped to ask a human before continuing) {#post-api-apps-appid-escalation-entryid-respond}

`POST /api/apps/:appId/escalation/:entryId/respond`  (Irreversible)  (Assistant)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
:::

- Answers a question a Digital Human asked during a run (an activity entry of type `escalation`; the Digital Human's status is `waiting_user` while it waits). Send `choice`, `text`, or both, as the question asked for; for a question with several parts, send `answers`, one `{choice?, text?}` per part, in order.
- This is the human decision the run was waiting for. Send the user's actual answer: it cannot be withdrawn afterwards.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |
| `entryId` | Activity entry id, from [`GET /api/apps/:appId/activity`](#get-api-apps-appid-activity). |

**Request body**

```json
{
  "choice": "approve",
  "text": "optional free-text explanation"
}
```

**Response**

```ts
{success:true,data:{…the answered activity entry}} | {success:false,error}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/escalation/<entryId>/respond" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"choice":"approve","text":"optional free-text explanation"}'
```

## Change how often a digital human runs, without editing the rest of its definition (spec) {#post-api-apps-appid-frequency}

`POST /api/apps/:appId/frequency`  (Reversible)  (Assistant)

- frequency can also be a cron expression (e.g. "0 8 * * *"); either one replaces the other. subscriptionId must name an existing schedule subscription — read app.spec.subscriptions via GET /:appId first. This rewrites that subscription's schedule in the spec, which is the only interval the scheduler reads and the one the settings panel shows; the rest of the spec is untouched.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "subscriptionId": "<subscriptionId — read app.spec.subscriptions via GET /api/apps/<appId>>",
  "frequency": "30m"
}
```

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/frequency" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"subscriptionId":"<subscriptionId — read app.spec.subscriptions via GET /api/apps/<appId>>","frequency":"30m"}'
```

## Clear a digital human's chat history in a bound IM chat {#post-api-apps-appid-im-chat-clear}

`POST /api/apps/:appId/im-chat/clear`  (Irreversible)  (Assistant)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
:::

- All four body fields are required, 400 otherwise. channel, chatType and chatId are checked as for GET im-chat/messages; chatType is "direct" or "group".

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "spaceId": "<spaceId — the app's own spaceId from GET /api/apps>",
  "channel": "wecom-bot",
  "chatType": "direct",
  "chatId": "<chatId — from GET /api/im-sessions>"
}
```

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/im-chat/clear" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId — the app'\''s own spaceId from GET /api/apps>","channel":"wecom-bot","chatType":"direct","chatId":"<chatId — from GET /api/im-sessions>"}'
```

## Read a digital human's transcript in a bound IM chat {#get-api-apps-appid-im-chat-messages}

`GET /api/apps/:appId/im-chat/messages`  (Assistant)

- All three query params are required, 400 otherwise. spaceId must be the app's own — a different but valid one is accepted and reads the wrong space. Optional ?chatType=group, defaults to direct.
- channel is the session's IM channel (wecom-bot, feishu-bot, dingtalk-bot, weixin-ilink-bot); chatId is taken as [`GET /api/im-sessions`](https://hello-halo.cc/docs/en/developers/api/channels.html#get-api-im-sessions) gives it. Any other channel, chatType or a chatId with whitespace, ":", "/", "\" or ".." is a 400. Read "http" and "local" sessions with [`GET /api/apps/:appId/chat/messages`](#get-api-apps-appid-chat-messages)?conversationId=.
- This is real correspondence with a person on the other end — read it only when the task needs it.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Query parameters**: `channel` (required), `chatId` (required), `spaceId` (required), `chatType`

**Response**

```ts
{success:true,data:[{id,role:"user"|"assistant",content,timestamp,thoughts?,thoughtsSummary?,images?}]}
```

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/im-chat/messages?channel=wecom-bot&chatId=<chatId — from GET /api/im-sessions>&spaceId=<spaceId — the app's own spaceId from GET /api/apps>" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Stop a digital human's in-progress reply in a bound IM chat (keeps history) {#post-api-apps-appid-im-chat-stop}

`POST /api/apps/:appId/im-chat/stop`  (Assistant)

- channel, chatType and chatId are checked as for GET im-chat/messages; chatType is "direct" or "group". Unlike im-chat/clear, this only aborts the current turn — the session and its history continue on the next inbound message.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "channel": "wecom-bot",
  "chatType": "direct",
  "chatId": "<chatId — from GET /api/im-sessions>"
}
```

**Response**

```ts
{success:true,data:{stopped:boolean}}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/im-chat/stop" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"channel":"wecom-bot","chatType":"direct","chatId":"<chatId — from GET /api/im-sessions>"}'
```

## Read the state of a digital human's memory {#get-api-apps-appid-memory}

`GET /api/apps/:appId/memory`  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{exists,totalBytes,topicCount,lastConsolidatedAt,lastAttempt,consolidating}}
```

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/memory" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Consolidate a digital human's memory now {#post-api-apps-appid-memory-consolidate}

`POST /api/apps/:appId/memory/consolidate`  (Reversible)  (Assistant)

- Returns once started; runs in the background with the digital human's model. Keeps a restorable snapshot.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{started:boolean,reason?:"already-running"|"empty"|"not-found"}}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/memory/consolidate" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Set which space a digital human starts new work in {#post-api-apps-appid-move-space}

`POST /api/apps/:appId/move-space`  (Reversible)  (Assistant)

- Changes the default only: work already running, existing chat sessions and team work keep the environment they started in. A digital human must belong to a space, so newSpaceId is required — null is rejected. Nothing is stopped or restarted. 404 if appId does not exist; 400 if newSpaceId is an empty string.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "newSpaceId": "<spaceId — a uuid from GET /api/spaces>"
}
```

**Response**

```ts
{success:true,data:{}}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/move-space" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"newSpaceId":"<spaceId — a uuid from GET /api/spaces>"}'
```

## Merge-patch per-installation overrides (notification level, model) {#patch-api-apps-appid-overrides}

`PATCH /api/apps/:appId/overrides`  (Reversible)  (Assistant)

- JSON Merge Patch: send only the fields to change; `null` clears one (for example `{"modelSourceId":null}` falls back to the global model).
- Fields: `notificationLevel`, `modelSourceId`, `modelId`, `spaceMemoryAccess` (true lets the Digital Human read its workspace's memory topics), `chatReasoningEffort` (`off` to `max`, for its chats, IM replies and runs), `memory` (`{enabled, autoConsolidate, cadence}`, always the whole object; `cadence` is `diligent`, `balanced` or `economical`) and `disabledTools` (MCP apps only: tool names to leave out, the whole list each time; `[]` or `null` turns them all back on).
- A `modelSourceId` for an account that no longer exists is accepted; the Digital Human then runs on the current global account until the override changes.
- The run schedule is not set here; use [`POST /api/apps/:appId/frequency`](#post-api-apps-appid-frequency).

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "notificationLevel": "important"
}
```

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX PATCH "$HALO_URL/api/apps/<appId>/overrides" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"notificationLevel":"important"}'
```

## Pause a digital human (stops scheduling) {#post-api-apps-appid-pause}

`POST /api/apps/:appId/pause`  (Reversible)  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true}
```

**Errors**

- 404-equivalent: manager.pause throws for an unknown appId

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/pause" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Permanently delete a digital human and all its data {#delete-api-apps-appid-permanent}

`DELETE /api/apps/:appId/permanent`  (Irreversible)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
If you only need something narrower, use [`DELETE /api/apps/:appId`](#delete-api-apps-appid)
:::

- Deletes the Digital Human and all its data. It must be uninstalled first with [`DELETE /api/apps/:appId`](#delete-api-apps-appid); otherwise the call fails.
- Built-in Digital Humans cannot be deleted. Failures carry `errorName` so a client can tell the cases apart.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true} | {success:false,error,errorName}
```

**Example**

```bash
curl -sX DELETE "$HALO_URL/api/apps/<appId>/permanent" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Grant a named permission to a digital human {#post-api-apps-appid-permissions-grant}

`POST /api/apps/:appId/permissions/grant`  (Reversible)  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "permission": "ai-browser"
}
```

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/permissions/grant" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"permission":"ai-browser"}'
```

## Revoke a named permission from a digital human {#post-api-apps-appid-permissions-revoke}

`POST /api/apps/:appId/permissions/revoke`  (Reversible)  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "permission": "ai-browser"
}
```

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/permissions/revoke" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"permission":"ai-browser"}'
```

## Reinstall a previously soft-deleted (uninstalled) digital human {#post-api-apps-appid-reinstall}

`POST /api/apps/:appId/reinstall`  (Reversible)  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{activationWarning?:string}} — activationWarning is set (non-fatal) if restarting it failed
```

**Errors**

- 404-equivalent: manager.reinstall throws if appId was never installed or is not currently uninstalled

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/reinstall" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## List the reminders a digital human set in its conversations {#get-api-apps-appid-reminders}

`GET /api/apps/:appId/reminders`  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:[{id,conversationId,conversation:{kind,name?},message,schedule,nextAt,createdAt,setBy?}]}
```

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/reminders" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Cancel a reminder a digital human set in one of its conversations {#delete-api-apps-appid-reminders-reminderid}

`DELETE /api/apps/:appId/reminders/:reminderId`  (Irreversible)  (Assistant)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
:::

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |
| `reminderId` | Reminder id, from [`GET /api/apps/:appId/reminders`](#get-api-apps-appid-reminders). |

**Response**

```ts
{success:true} | {success:false,error:"Reminder not found"}
```

**Example**

```bash
curl -sX DELETE "$HALO_URL/api/apps/<appId>/reminders/<reminderId>" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Resume a paused digital human {#post-api-apps-appid-resume}

`POST /api/apps/:appId/resume`  (Reversible)  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true}
```

**Errors**

- 404-equivalent: manager.resume throws for an unknown appId

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/resume" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Resume a run that stopped prematurely on an error {#post-api-apps-appid-runs-runid-continue}

`POST /api/apps/:appId/runs/:runId/continue`  (Irreversible)  (Assistant)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
:::

- Only valid for a run that ended in a premature-stop error, not any run. This re-opens the run with full tool permissions (bypassPermissions) and can take real-world action that Halo cannot undo.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |
| `runId` | Id of one Digital Human run, the runId in [`GET /api/apps/:appId/activity`](#get-api-apps-appid-activity). |

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/runs/<runId>/continue" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Inject a text message into a currently active run {#post-api-apps-appid-runs-runid-inject}

`POST /api/apps/:appId/runs/:runId/inject`  (Irreversible)  (Assistant)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
:::

- The message reaches the running Digital Human, which keeps its full tool permissions and can act on it in ways Halo cannot undo.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |
| `runId` | Id of one Digital Human run, the runId in [`GET /api/apps/:appId/activity`](#get-api-apps-appid-activity). |

**Request body**

```json
{
  "text": "Also check the March invoices"
}
```

**Response**

```ts
{success:true}
```

**Errors**

- 400 when `text` is missing or the run is not active.

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/runs/<runId>/inject" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"Also check the March invoices"}'
```

## Read what a run actually did, step by step {#get-api-apps-appid-runs-runid-session}

`GET /api/apps/:appId/runs/:runId/session`  (Assistant)

- The transcript behind the "View process" panel — same shape as chat/messages, but for one automation run. It is the only way to see why a run did what it did rather than what it reported.
- Get the runId from [`GET /api/apps/<appId>/activity`](#get-api-apps-appid-activity). Read this when a run went wrong; for what it concluded, its activity entry is shorter and enough.
- Only a person's newest 200 runs keep their transcript. An older run answers {success:false,code:"RUN_PROCESS_CLEARED"}; its activity entry still says what it did, and it can no longer be continued.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |
| `runId` | Id of one Digital Human run, the runId in [`GET /api/apps/:appId/activity`](#get-api-apps-appid-activity). |

**Response**

```ts
{success:true,data:[{id,role:"user"|"assistant",content,timestamp,thoughts?,thoughtsSummary?,images?}]}
```

**Errors**

- 404 when the app or its space is gone. An empty array means that run left no transcript, not that the ids were wrong.

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/runs/<runId>/session" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Start a new chat thread with a digital human {#post-api-apps-appid-sessions-create}

`POST /api/apps/:appId/sessions/create`  (Assistant)

- Returns a new thread's `conversationId`, ready to pass to [`POST /api/apps/:appId/chat/send`](#post-api-apps-appid-chat-send) and [`GET /api/apps/:appId/chat/messages`](#get-api-apps-appid-chat-messages). Give each end user a thread of their own to keep their contexts apart.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{conversationId}}  // already in the form chat/send takes
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/sessions/create" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Delete a chat thread with a digital human {#post-api-apps-appid-sessions-delete}

`POST /api/apps/:appId/sessions/delete`  (Irreversible)  (Assistant)

::: danger Irreversible
This cannot be undone, and nothing goes to a trash folder. Make sure it is what the user wants before you call it.
:::

- The id names one of this digital human's own threads, never a conversation from [`GET /api/spaces/<spaceId>/conversations`](https://hello-halo.cc/docs/en/developers/api/conversation.html#get-api-spaces-spaceid-conversations).

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "spaceId": "<spaceId — the app's own spaceId from GET /api/apps>",
  "conversationId": "<conversationId — a thread of THIS app, from GET /api/im-sessions?appId=>"
}
```

**Response**

```ts
{success:true}
```

**Errors**

- 400 if spaceId or conversationId is missing.

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/sessions/delete" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId — the app'\''s own spaceId from GET /api/apps>","conversationId":"<conversationId — a thread of THIS app, from GET /api/im-sessions?appId=>"}'
```

## Branch an existing chat thread into a new one, copying its history {#post-api-apps-appid-sessions-fork}

`POST /api/apps/:appId/sessions/fork`  (Assistant)

- A digital human's threads are not the conversations in a space — do not take an id from [`GET /api/spaces/<spaceId>/conversations`](https://hello-halo.cc/docs/en/developers/api/conversation.html#get-api-spaces-spaceid-conversations), it belongs to the user's chats with the Halo assistant and this route will not find it.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "spaceId": "<spaceId — the app's own spaceId from GET /api/apps>",
  "sourceConversationId": "<conversationId — a thread of THIS app, from POST /api/apps/<appId>/sessions/create or assembled from GET /api/im-sessions?appId=>"
}
```

**Response**

```ts
{success:true,data:{conversationId}}
```

**Errors**

- 400 if spaceId or sourceConversationId is missing.

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/sessions/fork" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId — the app'\''s own spaceId from GET /api/apps>","sourceConversationId":"<conversationId — a thread of THIS app, from POST /api/apps/<appId>/sessions/create or assembled from GET /api/im-sessions?appId=>"}'
```

## Edit part of a digital human's definition (spec) without replacing all of it {#patch-api-apps-appid-spec}

`PATCH /api/apps/:appId/spec`  (Reversible)

- JSON Merge Patch on the top-level spec fields: each field you send replaces the stored one, `null` removes it, and the merged spec is validated again.
- A changed `subscriptions` takes effect without interrupting a run in progress. To change only how often it runs, [`POST /api/apps/:appId/frequency`](#post-api-apps-appid-frequency) is simpler.
- A build can refuse remote changes that touch MCP apps (403).

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "description": "Checks the product page every weekday at 9:00"
}
```

**Response**

```ts
{success:true} | {success:false,error}
```

**Example**

```bash
curl -sX PATCH "$HALO_URL/api/apps/<appId>/spec" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"description":"Checks the product page every weekday at 9:00"}'
```

## Get real-time run state of a digital human (running/queued/idle/etc.) {#get-api-apps-appid-state}

`GET /api/apps/:appId/state`  (Assistant)

- More detail than the "status" field on GET /:appId alone (adds nextRunAtMs, queued/idle distinction)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{status:"running"|"queued"|"idle"|"paused"|"waiting_user"|"error",nextRunAtMs?,runningRunId?,lastRunAtMs?,lastStatus?,lastError?,consecutiveErrors?,pendingEscalationId?}}
```

**Example**

```bash
curl -s "$HALO_URL/api/apps/<appId>/state" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Manually trigger a digital human to run immediately {#post-api-apps-appid-trigger}

`POST /api/apps/:appId/trigger`

- Runs the Digital Human now and answers only after the run has finished, which can take minutes. Use a long client timeout, or follow the run with [`GET /api/apps/:appId/state`](#get-api-apps-appid-state) and the WebSocket instead of waiting on this call.
- If a run is already in progress or queued, the call fails with that message.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Response**

```ts
{success:true,data:{appId,runId,sessionKey,outcome:"useful"|"noop"|"error"|"skipped",startedAt,finishedAt,durationMs,tokensUsed?,errorMessage?,finalText?}} | {success:false,error}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/trigger" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Set how a store-installed digital human handles version upgrades {#post-api-apps-appid-upgrade-strategy}

`POST /api/apps/:appId/upgrade-strategy`  (Reversible)  (Assistant)

- strategy is one of "auto" (silent patch/minor, notify on major), "notify" (always notify), or "manual" (no automatic checks)

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](#get-api-apps). |

**Request body**

```json
{
  "strategy": "auto"
}
```

**Response**

```ts
{success:true}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/<appId>/upgrade-strategy" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"strategy":"auto"}'
```

## Install a digital human (or other app type) from a YAML definition (spec) string {#post-api-apps-import-spec}

`POST /api/apps/import-spec`

- Installs a Digital Human (or another app type) from the text of a `spec.yaml`, validated the same way as in the app. Optional `userConfig` fills its configuration fields.
- Unlike installing in the Halo app or through its assistant, this call does not install the skills the spec requires and does not undo a half-finished install.
- A build can refuse remote installs of MCP apps (403).

**Request body**

```json
{
  "spaceId": "<spaceId>",
  "yamlContent": "<contents of spec.yaml>"
}
```

**Response**

```ts
{success:true,data:{appId}} | {success:false,error,code}
```

**Errors**

- 400 when `spaceId` or `yamlContent` is missing or invalid; other failures carry a `code`.

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/import-spec" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId>","yamlContent":"<contents of spec.yaml>"}'
```

## Install a digital human (or other app type) from its full definition (spec) object {#post-api-apps-install}

`POST /api/apps/install`

- Installs a Digital Human (or a skill or MCP app) from a full spec object, the parsed form of a `spec.yaml`. `spaceId: null` installs a skill or MCP app for every workspace. `userConfig` is optional.
- Unlike installing in the Halo app or through its assistant, this call does not install the skills the spec requires and does not undo a half-finished install. Check [`GET /api/apps/:appId`](#get-api-apps-appid) afterwards.

**Request body**

```ts
{"spaceId": "<spaceId> | null", "spec": {…the parsed spec.yaml…}, "userConfig": {…}}
```

**Response**

```ts
{success:true,data:{appId}} | {success:false,error,code?}
```

**Errors**

- 400 missing `spec` or a `spaceId` that is neither a string nor null, 403 `MCP_COMMAND_BLOCKED` (an MCP command this build's policy blocks), 409 `ALREADY_INSTALLED`, 503 `NOT_INITIALIZED`.

**Example**

```bash
curl -sX POST "$HALO_URL/api/apps/install" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId>","spec":{"name":"Price watcher","version":"1.0.0","author":"example","description":"Checks a product page every morning","type":"automation","system_prompt":"Check the product page and report price changes."}}'
```
