---
title: "Store API: browse, install and update"
description: "Halo HTTP API for the Store: browse and search listings, install and uninstall Digital Humans and skills, and manage Store sources and updates."
type: "reference"
audience: ["developer"]
lang: "en"
canonical: "https://hello-halo.cc/docs/en/developers/api/store.html"
last_updated: "2026-10-19T00:11:17+08:00"
source: "Halo 3.0.4 docs"
---
# API: Store

Halo HTTP API for the Store: browse and search listings, install and uninstall Digital Humans and skills, and manage Store sources and updates.

Browse and search the Store, read a listing, install and uninstall Digital Humans and skills, install from a local `.dhpkg` file, and manage Store sources and updates. Publishing happens in the Halo app; see [Publish to the Store](https://hello-halo.cc/docs/en/developers/store-publishing.html).

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 |
| --- | --- |
| [Set how a store-installed digital human handles version upgrades](https://hello-halo.cc/docs/en/developers/api/digital-human.html#post-api-apps-appid-upgrade-strategy) (Digital Humans) | `POST /api/apps/:appId/upgrade-strategy` |
| [Preview the slash-command name a skill title would install under](#get-api-skills-command-name) | `GET /api/skills/command-name` |
| [Get a listing's SKILL.md/README document](#get-api-store-app-document) | `GET /api/store/app-document` |
| [List store listings (legacy, non-paginated; prefer POST /api/store/query)](#get-api-store-apps) | `GET /api/store/apps` |
| [Get full detail for one store listing by slug](#get-api-store-apps-slug) | `GET /api/store/apps/:slug` |
| [Install an app from the store by slug (REST-style path variant)](#post-api-store-apps-slug-install) | `POST /api/store/apps/:slug/install` |
| [Get which store features this build supports (publish, reviews, install counts)](#get-api-store-capabilities) | `GET /api/store/capabilities` |
| [Get the store category enum used for browsing and publishing](#get-api-store-category-taxonomy) | `GET /api/store/category-taxonomy` |
| [Get the curated store home page (featured/trending sections)](#get-api-store-discover-page) | `GET /api/store/discover-page` |
| [Dismiss an available update so it stops appearing as pending](#post-api-store-ignore-version) | `POST /api/store/ignore-version` |
| [Install a digital human from a local .dhpkg file](#post-api-store-import-dhpkg) | `POST /api/store/import-dhpkg` |
| [Install an app from the store by slug](#post-api-store-install) | `POST /api/store/install` |
| [List the signed-in creator's own published apps](#get-api-store-my-publications) | `GET /api/store/my-publications` |
| [Find the installed app that a store slug would resolve to](#post-api-store-publish-find-app) | `POST /api/store/publish/find-app` |
| [Preview the slug and version a publish of an app would produce](#post-api-store-publish-preview) | `POST /api/store/publish/preview` |
| [Paginated search over store listings (primary browse endpoint)](#post-api-store-query) | `POST /api/store/query` |
| [Force a full re-download of the store registry index](#post-api-store-refresh) | `POST /api/store/refresh` |
| [List configured store registry sources](#get-api-store-registries) | `GET /api/store/registries` |
| [Add a new registry source](#post-api-store-registries) | `POST /api/store/registries` |
| [Remove a configured registry source](#delete-api-store-registries-registryid) | `DELETE /api/store/registries/:registryId` |
| [Update a registry source's adapter config (e.g. its API key)](#patch-api-store-registries-registryid-adapter-config) | `PATCH /api/store/registries/:registryId/adapter-config` |
| [Enable or disable a configured registry source](#post-api-store-registries-registryid-toggle) | `POST /api/store/registries/:registryId/toggle` |
| [Cheap conditional freshness check for the store index](#post-api-store-revalidate) | `POST /api/store/revalidate` |
| [Take down one of the creator's own published apps](#post-api-store-unpublish) | `POST /api/store/unpublish` |
| [List available updates for installed apps](#get-api-store-updates) | `GET /api/store/updates` |
| [Apply an available update to an installed app](#post-api-store-updates-appid-apply) | `POST /api/store/updates/:appId/apply` |
| [Re-check all installed apps for available updates right now](#post-api-store-updates-check-now) | `POST /api/store/updates/check-now` |

## Preview the slash-command name a skill title would install under {#get-api-skills-command-name}

`GET /api/skills/command-name`  (Assistant)

- Query: ?name=. A missing name returns an empty string, not a 400. Pure string transform — it does not check whether that name is already taken.

**Query parameters**: `name`

**Response**

```ts
{success:true,data:string}  // e.g. "My Skill" -> "my-skill"
```

**Example**

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

## Get a listing's SKILL.md/README document {#get-api-store-app-document}

`GET /api/store/app-document`  (Assistant)

- slug is a query param (?slug=...), not a path segment — scoped slugs like "owner/repo" contain "/". content is null when the source has no document.

**Query parameters**: `slug`

**Response**

```ts
{success:true,data:{content:string|null}}
```

**Example**

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

## List store listings (legacy, non-paginated; prefer POST /api/store/query) {#get-api-store-apps}

`GET /api/store/apps`  (Assistant)

- Query params: search, locale, category, type, tags (comma-separated).

**Query parameters**: `category`, `locale`, `search`, `tags`, `type`

**Response**

```ts
{success:true,data:[{slug,name,version,author,description,type,category,tags,...}]}
```

**Example**

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

## Get full detail for one store listing by slug {#get-api-store-apps-slug}

`GET /api/store/apps/:slug`  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `slug` | Store listing slug, from [`GET /api/store/apps`](#get-api-store-apps). |

**Response**

```ts
{success:true,data:{entry:{slug,name,version,...},spec:<AppSpec>,registryId}} or {success:false,error}
```

**Example**

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

## Install an app from the store by slug (REST-style path variant) {#post-api-store-apps-slug-install}

`POST /api/store/apps/:slug/install`  (Reversible)  (Assistant)

- Always send `spaceId`. It is used exactly as sent: leave it out and the item installs for every workspace; name another workspace and it installs there. Both answer `success: true`, and the response does not say where it went.
- Send `"spaceId": null` only for a deliberate global install of a skill or MCP app. Optional field: `userConfig` (object).
- Same as [`POST /api/store/install`](#post-api-store-install) with the slug in the path, including the 403 `MCP_COMMAND_BLOCKED` case.

**Path parameters**

| Name | Description |
| --- | --- |
| `slug` | Store listing slug, from [`GET /api/store/apps`](#get-api-store-apps). |

**Request body**

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

**Response**

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

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/apps/<slug>/install" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"<spaceId>"}'
```

## Get which store features this build supports (publish, reviews, install counts) {#get-api-store-capabilities}

`GET /api/store/capabilities`  (Assistant)

- Reports which Store features this build supports. Publishing happens in the Halo app, not through this API.

**Response**

```ts
{success:true,data:{catalog:true,installs:boolean,publish:boolean,reviewWorkflow:boolean,identity:"none"|string}}
```

**Example**

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

## Get the store category enum used for browsing and publishing {#get-api-store-category-taxonomy}

`GET /api/store/category-taxonomy`  (Assistant)

- Categories are keyed per app type (skill/automation/mcp/extension) with "default" as the fallback list for types without their own entry.

**Response**

```ts
{success:true,data:{default:[{id,label|labelKey,icon?}],<appType>?:[...]}}
```

**Example**

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

## Get the curated store home page (featured/trending sections) {#get-api-store-discover-page}

`GET /api/store/discover-page`  (Assistant)

- Query params: locale?, pageSize?. Falls back to a single paginated catalog node when no curated layout is configured.

**Query parameters**: `locale`, `pageSize`

**Response**

```ts
{success:true,data:{version,nodes:[{type,title?,subtitle?,entries?:[RegistryEntry],collections?,hasMore?}]}}
```

**Example**

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

## Dismiss an available update so it stops appearing as pending {#post-api-store-ignore-version}

`POST /api/store/ignore-version`  (Reversible)  (Assistant)

- Only hides that one version; a newer update still surfaces normally.

**Request body**

```json
{
  "appId": "<appId — a uuid from GET /api/apps>",
  "version": "1.2.0"
}
```

**Response**

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

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/ignore-version" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"appId":"<appId — a uuid from GET /api/apps>","version":"1.2.0"}'
```

## Install a digital human from a local .dhpkg file {#post-api-store-import-dhpkg}

`POST /api/store/import-dhpkg`  (Reversible)  (Assistant)

- Installs and activates the package's Digital Human; it then runs on its own schedule. Undo with [`DELETE /api/apps/:appId`](https://hello-halo.cc/docs/en/developers/api/digital-human.html#delete-api-apps-appid).
- Unlike installing in the Halo app or through its assistant, this does not fetch the package's required skills or undo a half-finished install. Check [`GET /api/apps/:appId`](https://hello-halo.cc/docs/en/developers/api/digital-human.html#get-api-apps-appid) afterwards; an activation that fails after the install still answers `success: true`.
- `filePath` is a path on the computer running Halo. `spaceId` is used exactly as sent: left out means an install for every workspace, another workspace installs there, and neither is reported back.

**Request body**

```json
{
  "filePath": "/absolute/path/to/app.dhpkg",
  "spaceId": "<spaceId>"
}
```

**Response**

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

**Errors**

- 400 when `filePath` is missing.

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/import-dhpkg" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filePath":"/absolute/path/to/app.dhpkg","spaceId":"<spaceId>"}'
```

## Install an app from the store by slug {#post-api-store-install}

`POST /api/store/install`  (Reversible)  (Assistant)

- Always send `spaceId`. It is used exactly as sent: leave it out and the item installs for every workspace; name another workspace and it installs there. Both answer `success: true`, and the response does not say where it went.
- Send `"spaceId": null` only for a deliberate global install of a skill or MCP app. Optional field: `userConfig` (object).
- Same operation as [`POST /api/store/apps/:slug/install`](#post-api-store-apps-slug-install), with the slug in the body.

**Request body**

```json
{
  "slug": "author/app-id",
  "spaceId": "<spaceId>"
}
```

**Response**

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

**Errors**

- 400 = missing slug.
- 403 with `code: "MCP_COMMAND_BLOCKED"` when the item is an MCP server this build's policy blocks.

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/install" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug":"author/app-id","spaceId":"<spaceId>"}'
```

## List the signed-in creator's own published apps {#get-api-store-my-publications}

`GET /api/store/my-publications`  (Assistant)

- Fails when no creator identity is signed in — this build may not support publishing at all (check [`GET /api/store/capabilities`](#get-api-store-capabilities) first).

**Response**

```ts
{success:true,data:[{slug,name?,displayName?,type?,version,status:"listed"|"hidden"|"rejected",takedownBy?,rejectReason?,submittedAt?,channel?,installs?}]}
```

**Example**

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

## Find the installed app that a store slug would resolve to {#post-api-store-publish-find-app}

`POST /api/store/publish/find-app`  (Assistant)

- Optional fields: type, author. appId is null when no locally installed app uniquely matches.

**Request body**

```json
{
  "slug": "author/app-id"
}
```

**Response**

```ts
{success:true,data:{appId: string|null}}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/publish/find-app" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug":"author/app-id"}'
```

## Preview the slug and version a publish of an app would produce {#post-api-store-publish-preview}

`POST /api/store/publish/preview`  (Assistant)

- Optional fields: author, name (override the store name before deriving the slug). storeVersion is null when the slug has never been published.

**Request body**

```json
{
  "appId": "<appId — a uuid from GET /api/apps>"
}
```

**Response**

```ts
{success:true,data:{slug,localVersion,storeVersion:string|null}}
```

**Example**

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

## Paginated search over store listings (primary browse endpoint) {#post-api-store-query}

`POST /api/store/query`  (Assistant)

- All fields optional: search, type, category, page, pageSize, locale.

**Request body**

```json
{
  "search": "weather",
  "page": 1,
  "pageSize": 20
}
```

**Response**

```ts
{success:true,data:{items:[{slug,name,version,author,description,type,category,tags,...}],total?,hasMore,groups?:[{type,count,hasMore}],sources:[{registryId,status,error?}]}}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/query" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"search":"weather","page":1,"pageSize":20}'
```

## Force a full re-download of the store registry index {#post-api-store-refresh}

`POST /api/store/refresh`  (Assistant)

- Heavier than [`POST /api/store/revalidate`](#post-api-store-revalidate) — use revalidate for a cheap freshness check.

**Response**

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

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/refresh" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## List configured store registry sources {#get-api-store-registries}

`GET /api/store/registries`  (Assistant)

- `adapterConfig` is returned as stored and can hold an API key in plain text. Treat the response like a secret.

**Response**

```ts
{success:true,data:[{id,name,url,enabled,isDefault?,sourceType?,adapterConfig?}]}
```

**Example**

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

## Add a new registry source {#post-api-store-registries}

`POST /api/store/registries`  (Assistant)

- Optional fields: `sourceType` (string) and `adapterConfig` (object, adapter-specific, for example an API key). The response returns `adapterConfig` as stored.
- `url` must be http(s) and must not point to a loopback, private or link-local host. Fails if a source with the same URL already exists.

**Request body**

```json
{
  "name": "My Registry",
  "url": "https://example.com/registry"
}
```

**Response**

```ts
{success:true,data:{id,name,url,enabled,isDefault:false,sourceType?,adapterConfig?}} | {success:false,error}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/registries" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"My Registry","url":"https://example.com/registry"}'
```

## Remove a configured registry source {#delete-api-store-registries-registryid}

`DELETE /api/store/registries/:registryId`  (Reversible)  (Assistant)

- Fails with an error (not thrown) when the target is the built-in/default registry — those cannot be removed. Can be added back with [`POST /api/store/registries`](#post-api-store-registries).

**Path parameters**

| Name | Description |
| --- | --- |
| `registryId` | Store source id, from [`GET /api/store/registries`](#get-api-store-registries). |

**Response**

```ts
{success:true,data:undefined} or {success:false,error}
```

**Example**

```bash
curl -sX DELETE "$HALO_URL/api/store/registries/<registryId>" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Update a registry source's adapter config (e.g. its API key) {#patch-api-store-registries-registryid-adapter-config}

`PATCH /api/store/registries/:registryId/adapter-config`  (Reversible)  (Assistant)

- Body is a flat JSON object of adapter-specific fields — exact keys depend on the registry's sourceType.
- Write-only: the response never echoes the config back, so this is safe for setting credentials. 400 if the body is missing or not a plain object.

**Path parameters**

| Name | Description |
| --- | --- |
| `registryId` | Store source id, from [`GET /api/store/registries`](#get-api-store-registries). |

**Request body**

```json
{
  "apiKey": "sk-..."
}
```

**Response**

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

**Example**

```bash
curl -sX PATCH "$HALO_URL/api/store/registries/<registryId>/adapter-config" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"apiKey":"sk-..."}'
```

## Enable or disable a configured registry source {#post-api-store-registries-registryid-toggle}

`POST /api/store/registries/:registryId/toggle`  (Reversible)  (Assistant)

**Path parameters**

| Name | Description |
| --- | --- |
| `registryId` | Store source id, from [`GET /api/store/registries`](#get-api-store-registries). |

**Request body**

```json
{
  "enabled": true
}
```

**Response**

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

**Errors**

- 400 if enabled is missing or not a boolean.

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/registries/<registryId>/toggle" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":true}'
```

## Cheap conditional freshness check for the store index {#post-api-store-revalidate}

`POST /api/store/revalidate`  (Assistant)

- changed:false means the index did not move (a 304-equivalent); the browse list does not need refetching.

**Response**

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

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/revalidate" \
  -H "Authorization: Bearer $HALO_TOKEN"
```

## Take down one of the creator's own published apps {#post-api-store-unpublish}

`POST /api/store/unpublish`  (Reversible)  (Assistant)

- Can be republished later; already-installed/downloaded copies elsewhere are not affected either way.

**Request body**

```json
{
  "slug": "author/app-id"
}
```

**Response**

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

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/unpublish" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug":"author/app-id"}'
```

## List available updates for installed apps {#get-api-store-updates}

`GET /api/store/updates`  (Assistant)

**Response**

```ts
{success:true,data:[{appId,currentVersion,latestVersion,entry,strategy,severity}]}
```

**Example**

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

## Apply an available update to an installed app {#post-api-store-updates-appid-apply}

`POST /api/store/updates/:appId/apply`  (Reversible)  (Assistant)

- mode is optional: "patch_minor"|"major"|"force", defaults to "force". No built-in downgrade path — reinstalling the old version is the way back.
- A digital human keeps the user's version of every field that differs from the author's new version (prompt, run schedule, name, ...); kept lists those fields, empty when the update applied in full. editsKnown false means there was no earlier author version to compare with, so every difference was kept. The user can switch kept fields to the author's version in the app.

**Path parameters**

| Name | Description |
| --- | --- |
| `appId` | Digital Human id, from [`GET /api/apps`](https://hello-halo.cc/docs/en/developers/api/digital-human.html#get-api-apps). |

**Request body**

```json
{
  "mode": "force"
}
```

**Response**

```ts
{success:true,data:{appId,from,to,severity,kept,editsKnown}} or {success:false,error}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/updates/<appId>/apply" \
  -H "Authorization: Bearer $HALO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"force"}'
```

## Re-check all installed apps for available updates right now {#post-api-store-updates-check-now}

`POST /api/store/updates/check-now`  (Reversible)  (Assistant)

- Apps set to auto-update silently apply any patch/minor update found — this is not just a read.

**Response**

```ts
{success:true,data:[{appId,currentVersion,latestVersion,entry,strategy,severity}]}
```

**Example**

```bash
curl -sX POST "$HALO_URL/api/store/updates/check-now" \
  -H "Authorization: Bearer $HALO_TOKEN"
```
