# Apps

> Manage your apps and versions via REST.

**URL:** https://inference.sh/docs/api/rest/apps
**Last updated:** 2026-10-11

---

Manage your apps and versions via REST.

For running apps (creating tasks), see [Tasks](/docs/api/rest/tasks). For CLI deploy workflows, see [Deploying](/docs/extend/deploying).

→ [Apps concept](/docs/concepts/apps) · [Extending Apps](/docs/extend/extending-apps)

---

## Authentication

Read endpoints require the `apps:read` scope. Write endpoints (including setting the current version) require `apps:write`.

---

## App refs and naming

Apps are identified by **`namespace/name`** — for example `myteam/image-gen`. This ref is what callers pass to `POST /run`, the CLI (`belt app run namespace/name`), and agent tools.

| Rule | Detail |
|------|--------|
| **Unique ref** | Each `namespace/name` pair is unique among live apps. Creating or deploying to a taken ref returns **409** `name_conflict`. |
| **Same name, different namespace** | The same `name` slug can exist in multiple namespaces — even under the same owner (for example `arxiv/search` and `exa/search`). |
| **Updates** | Redeploying to an existing ref creates a new version instead of a second app — see [Deploying](/docs/extend/deploying#update). |
| **Deleted apps** | Deleting an app releases its `namespace/name` ref for reuse. |

Pin a specific version with `@version-id` (for example `myteam/image-gen@ver_abc123`). Omit the version pin to use the current active version.

---

## Create or deploy app

`POST /apps`

Registers a new app or deploys a new version to an existing `namespace/name` ref. The CLI uses this endpoint for `belt app deploy`. Requires the `apps:write` scope.

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | No | App ID — include to deploy a new version of a known app instead of matching by `namespace/name` |
| `name` | string | Yes | App slug (with `namespace`, forms the ref) |
| `namespace` | string | No | Ignored for everyone except platform admins: the app is always created in the caller's workspace namespace. |
| `title` | string | No | Display title |
| `description` | string | No | Store listing blurb — max [200 characters](/docs/api/rest/overview#description-length-limits) |
| `agent_description` | string | No | Short text for agent tool catalogs |
| `category` | string | No | Store category |
| `images` | object | No | Store card, thumbnail, and banner URLs |
| `tags` | string[] | No | Replace the app's tags. Omitted, deploy uses the version's `metadata.tags`, and keeps the stored tags when that is empty too |
| `version` | object | No | App version payload (kernel, code bundle, schemas, credentials, …) |
| `preserve_current_version` | boolean | No | When redeploying, keep the current active version (staged deploy). Default `false` promotes the new version. |

When `namespace/name` already exists, the call creates a **new version** on that app (same as [redeploying](/docs/extend/deploying#update)). Omitting `description` on a redeploy keeps the stored listing text.

### Errors

| HTTP | Code | When |
|------|------|------|
| 400 | `validation_error` | Invalid input — for example `description` longer than the [listing limit](/docs/api/rest/overview#description-length-limits). `detail` names the field and limit. No app row or version is created. |
| 403 | `forbidden` | Caller lacks write access on the app |
| 402 / 403 | `limit_exceeded`, `feature_not_available`, … | Plan entitlements block creating a new private app — see [Entitlements](/docs/api/rest/entitlements) |

CLI equivalent: `belt app deploy` (add `--stage` to set `preserve_current_version`).

---

## Get App

`GET /apps/{id}`

Returns a single app by ID, including the active version in `version_id` and `version`.

`GET /apps/{namespace}/{name}`

Returns an app by namespace and name (for example `myteam/my-app`). The handler resolves **ref routes** (platform aliases): the JSON describes the **target** app (`namespace`, `name`, `id`, and `version` are the resolved target, which may differ from the path you requested). When the ref, or the alias it resolves through, pins a [multi-function](/docs/extend/multi-function-apps) entry point (for example `elevenlabs/text-to-dialogue` → `elevenlabs/eleven-v3:dialogue`), `resolved_function` names it. `POST /apps/run` and the CLI already honour a pinned function; `resolved_function` lets clients open the same entry point without guessing from the target app's default.

**Web app aliases:** Public `GET /ref-routes/site` returns the marketing redirect table (`redirects` map: site path → site path or URL). For redirect-mode app aliases whose target ref pins a function, the destination path includes `?function=<name>` (for example `/apps/elevenlabs/eleven-v3?function=dialogue`).

Returns **404** with code `not_found` when no app matches; use [Search](/docs/api/rest/search) for broader discovery.

App objects include listing metadata used by the Grid, [store API](/docs/api/rest/store), and search:

| Field | Type | Description |
|-------|------|-------------|
| `category` | string | Grid grouping: `image`, `video`, `audio`, `text`, `chat`, `3d`, `flow`, `decision`, or `other` |
| `tags` | string[] | Lowercase slugs naming tasks and traits (for example `text-to-image`, `open-weights`). Omitted when empty. |
| `resolved_function` | string | Present on **ref lookup** responses (`GET /apps/{namespace}/{name}` and refs that pin a function, for example `myteam/dialogue:stream`). Names the function the server resolved from the ref or a retired-name route. Empty means the client chooses, starting from the version's `default_function`. Not set on `AppVersionDTO` — use `default_function` there. |
| `images` | object | Store card, thumbnail, and banner URLs |
| `title` | string | Display name; falls back to `name` when empty |

### App category

| Value | Meaning |
|-------|---------|
| `image`, `video`, `audio`, `text`, `chat`, `3d`, `flow`, `other` | Standard Grid groupings — see [Configuration — Categories](/docs/extend/configuration#categories) |
| `decision` | Decision models: structured questions in, a probability distribution over answers out — no generative media output |

Decision apps use the `DecisionInput` / `DecisionOutput` JSON contract (choice, score, and noul questions keyed by `id`). Vision models add `images`; video models add `videos`, after the images. See [Decision apps](/docs/concepts/apps#decision-apps) and [JavaScript SDK — Decision app types](/docs/api/sdk-javascript#decision-app-types).

When you change an app's `category`, its public store listing's Grid category follows on the next app update or deploy.

### App tags

Tags name what an app does (`text-to-speech`, `image-editing`) and how it behaves (`realtime`, `open-weights`). They are indexed for [search](/docs/api/rest/search) and returned on store listings.

**Normalization:** tags are lowercased, trimmed, deduplicated (first occurrence wins), and must match `^[a-z0-9]+(-[a-z0-9]+)*$`. At most **10** tags, each at most **50** characters. Invalid tags in an API request return **400** validation.

**Setting tags:**

| Path | Behavior |
|------|----------|
| `tags` on `POST /apps` or deploy | When provided, **replaces** the app's stored tags |
| Omitted on deploy | Taken from the new version's `metadata.tags` in `inf.yml` (invalid entries skipped); if that is empty, existing app tags are kept |
| `belt app deploy --meta` | Does not change tags — use a full deploy or set `tags` via the API |

**Browseable tags** are slugs that at least **three public apps** carry. Fetch them with `GET /store/tags` ([Store API — List browseable tags](/docs/api/rest/store#list-browseable-tags)); each entry includes `slug`, `title`, and `count`. The set changes as public usage changes. Any slug that matches the format above is still valid on an app, in search, and in store `tag` filters even when it is not returned from `/store/tags` (for example `virtual-try-on` or `ocr`). For slugs not in that list, capitalize hyphen-separated words client-side or show the raw slug.

---

## List Apps

`GET /apps` or `POST /apps/list`

Returns your apps with cursor-based pagination. Requires the `apps:read` scope.

### Request

| Field | Type | Description |
|-------|------|-------------|
| `limit` | number | Page size (default 50, max 100) |
| `cursor` | string | Cursor from a previous response |
| `direction` | string | `"next"` (default) or `"prev"` |
| `filters` | array | `{ "field", "operator", "value" }` — use app column names |
| `sort` | array | `{ "field", "dir" }` where `dir` is `asc` or `desc` |
| `search` | object | `{ "term", "fields" }` for case-insensitive substring match |

Supported filter and sort fields include `namespace`, `name`, `category`, `visibility`, `status`, `created_at`, and `updated_at`. Unknown filter or sort columns return **400** (`Invalid sort or filter field`). See [Cursor pagination](/docs/api/rest/overview#cursor-pagination).

**Tag filter:** use field `tag` with operator `eq` and a single slug (for example `text-to-image`). Returns apps whose `tags` array includes that value, the same matching as the [public store list](/docs/api/rest/store#list-approved-apps).

```bash
curl -X POST https://api.inference.sh/apps/list \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "limit": 20,
    "filters": [{ "field": "tag", "operator": "eq", "value": "text-to-image" }]
  }'
```

**Text search:** `search.term` is required. Each name in `search.fields` must be an allowlisted column (`namespace`, `name`, `description`, `category`, `agent_description`, and others on the app model). Invalid or empty field names are ignored; if no valid fields remain, the search clause is skipped.

**Example — filter by namespace:**

```bash
curl -X POST https://api.inference.sh/apps/list \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "limit": 20,
    "filters": [{ "field": "namespace", "operator": "eq", "value": "myteam" }]
  }'
```

**Example — search name and description:**

```bash
curl -X POST https://api.inference.sh/apps/list \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "limit": 20,
    "search": { "term": "flux", "fields": ["name", "description"] }
  }'
```

### Response

| Field | Type | Description |
|-------|------|-------------|
| `items` | array | App objects |
| `next_cursor` | string | Cursor for the next page |
| `prev_cursor` | string | Cursor for the previous page |
| `has_next` | boolean | More results available |
| `has_previous` | boolean | Previous page available |

---

## List Versions

`GET /apps/{id}/versions` or `POST /apps/{id}/versions/list`

Returns version history for an app. Each version has its own ID.

---

## Get Version

`GET /apps/{id}/versions/{versionId}`

Returns a specific app version. `versionId` is the version's ID (the segment after `@` in refs like `myteam/my-app@ver_abc123`).

Deploy `warnings` are not stored on versions: they come back only on `version.warnings` in the `POST /apps` response that registered the version. See [Deploying: Warnings](/docs/extend/deploying#warnings).

### Custom UI page (`ui`)

An app version may name an [artifact](/docs/artifacts) page to render in place of the generic task output — in chat, the workspace app runner, and MCP hosts that support MCP Apps.

| Field | Location | Description |
|-------|----------|-------------|
| `ui.artifact` | Top-level on `AppVersionDTO` | Artifact ref or id (see below) |
| `metadata.ui` | Version metadata map | Same `{ "artifact": "…" }` object; `ui` mirrors it on read |

**Artifact ref formats:**

| Form | Behavior |
|------|----------|
| `namespace/name` | Uses the artifact's current published version |
| `namespace/name@version` | Pins one published version |
| `art_…` | Artifact id |

The publisher must be able to **read** the artifact when the version is created or updated; a missing or unreadable ref fails with **400** `validation_error`. The page must also be at least as visible as the app (a `public` app needs a public page, a workspace app a page the workspace can read), or the deploy is refused with the same error: whoever can read a chat that ran the app sees the page. Viewers who cannot read the artifact still run the app — they simply see the default output view instead of the custom page.

Set `ui` from the web app under **requirements → ui page**, or include `metadata.ui` when you register a version (deploy). Clear it by omitting `metadata.ui` or setting an empty artifact.

```json
{
  "metadata": {
    "ui": { "artifact": "myteam/run-dashboard" }
  }
}
```

---

## Update app metadata

Change listing fields on an app without redeploying code. Kernel and version metadata still come from [deploy](/docs/extend/deploying); use [Set current version](#set-current-version) to promote a version.

### Partial update (recommended)

`PATCH /apps/{id}`

Updates only the fields you include in the request body; all other app fields are left unchanged. Requires **`apps:write`**.

| Field | Type | Description |
|-------|------|-------------|
| `title` | string | Display name on the Grid and store |
| `description` | string | Short listing description |
| `agent_description` | string | Longer text for agent tool selection |
| `category` | string | Grid grouping — see [App category](#app-category) |
| `images` | object | Store card, thumbnail, and banner URLs |
| `tags` | string[] | Replaces the app's tag list — see [App tags](#app-tags) |

```bash
curl -X PATCH "https://api.inference.sh/apps/app_abc123" \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"title":"Renamed app","tags":["text-to-image","open-weights"]}'
```

Returns the updated app (same shape as [Get App](#get-app)).

`version_id`, lifecycle **`status`**, and **`visibility`** are not updated by this endpoint — use [Set current version](#set-current-version), [Update status](#update-status), and the visibility endpoint instead. Sending only system-managed keys (`id`, `short_id`, `user_id`, `team_id`, `created_at`, `deleted_at`) returns **400** `invalid_request`. Any other JSON key not in the table above returns **400** validation.

### Full update

`POST /apps/{id}`

Replaces editable app columns from the request body. Requires **`apps:write`**. For metadata-only changes, prefer **`PATCH`** so omitted fields are not cleared.

Omitted `tags` on `POST` keep the stored tag list; an empty `tags` array clears tags. Like `PATCH`, this endpoint does not change `version_id` or lifecycle status — older clients that omitted those fields no longer clear the active version or take a live app offline.

---

## Set Current Version

`PUT /apps/{id}/versions/{versionId}/current`

Sets which version is **active** for the app. The active version is what runs when callers use `namespace/name` without `@version`, and what the Grid and search index expose as the live app.

Requires the `apps:write` scope. No request body. Idempotent: calling it again with the same version has no effect.

### Example

```bash
curl -X PUT "https://api.inference.sh/apps/app_abc123/versions/ver_xyz789/current" \
  -H "Authorization: Bearer inf_your_key"
```

### Response

Returns the updated app (same shape as [Get App](#get-app)), with `version_id` and `version` pointing at the promoted version.

```json
{
  "id": "app_abc123",
  "namespace": "myteam",
  "name": "my-app",
  "version_id": "ver_xyz789",
  "version": { "id": "ver_xyz789", "kernel": "python-3.11" }
}
```

### When to use this

- After a **staged deploy** (`belt app deploy --stage`) — promote the new version when you are ready
- To **roll back** — set an older version as current
- From automation — switch active versions without using the web UI

By default, `belt app deploy` (without `--stage`) makes the new version current automatically. Use staged deploys plus this endpoint when you want to test a version before promoting it.

### Migration from `POST /apps/{id}/current-version`

The previous endpoint accepted the version ID in the JSON body:

```bash
# Removed — do not use
curl -X POST "https://api.inference.sh/apps/app_abc123/current-version" \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"version_id": "ver_xyz789"}'
```

Use `PUT /apps/{id}/versions/{versionId}/current` instead, with `versionId` in the path.

---

## App lifecycle status

Apps have a `status` field separate from `visibility`. **Visibility** controls who can see the app (`public`, `team`, `private`). **Status** controls whether new runs can be created and how the app appears in discovery.

App objects include:

| Field | Type | Description |
|-------|------|-------------|
| `status` | string | `active`, `maintenance`, `deprecated`, or `retired` (default `active`) |
| `status_message` | string | Optional human-readable explanation shown to callers |
| `status_changed_at` | string | ISO timestamp of the last status change |

| Status | New runs | Discovery |
|--------|----------|-----------|
| `active` | Allowed | Listed in store and search (when public) |
| `deprecated` | Allowed — existing integrations keep working | Listed in store and search (when public) |
| `maintenance` | Blocked — `503` with `app_maintenance` | Still visible to owners; public store/search behavior unchanged |
| `retired` | Blocked — `410` with `app_retired` | Excluded from [store listings](/docs/api/rest/store) and [search](/docs/api/rest/search); historical tasks remain accessible |

Use `deprecated` when you want callers to migrate but still allow runs. Use `maintenance` for temporary outages. Use `retired` when an app should no longer be discoverable or runnable while preserving run history.

### Update status

`POST /apps/{id}/status`

Requires the `apps:write` scope. The caller must have write permission on the app.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | `active`, `maintenance`, `deprecated`, or `retired` |
| `message` | string | No | Stored in `status_message` and shown when runs are blocked |

Returns the updated app (same shape as [Get App](#get-app)).

```bash
curl -X POST "https://api.inference.sh/apps/app_abc123/status" \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"status": "deprecated", "message": "Use myteam/flux-v2 instead"}'
```

When an app is in `maintenance` or `retired`, `POST /run` and `POST /apps/run` return errors — see [Tasks API — Run Task errors](/docs/api/rest/tasks#errors).
