---
title: "Deploy a catalog app"
method: POST
path: "/platform/deployments"
tags: ["Platform"]
---

# Deploy a catalog app

`POST /platform/deployments`

## Request

- Base URL: `https://api.imprezahost.com/v1`
- URL: `https://api.imprezahost.com/v1/platform/deployments`
- Auth: API key in header `X-API-Key` and API key in header `X-API-Secret`

Installs an app from the curated catalog on the target agent.
Returns immediately with `status: pending`; poll
`GET /platform/deployments/{id}` until it reaches `running`
(typically 30 s – 3 min depending on the app).

You can install the **same app multiple times on one agent** — the
platform assigns each instance its own host port automatically, so
they coexist without conflict. (Fixed-protocol apps like RustDesk
are the exception — one per server.)

**Network mode** (declared on the catalog manifest, not passed
in the request — every catalog app picks one). Drives whether
the response carries a `domain` (HTTP path) or a
`connection_info: {ip, ports, public_key}` block (raw TCP/UDP
path) for reachability:

- `http` (default) — app speaks HTTP. Behind Caddy + Let's
  Encrypt at an `imprezaapps.com` subdomain (or a customer
  domain). The `domain` handling rules below apply.
- `host_ports` — app speaks raw TCP/UDP (RustDesk: 21115/tcp,
  21116/tcp + 21116/udp, 21117/tcp, 21118/tcp, 21119/tcp). No
  Caddy, no cert, no domain. Response carries `connection_info`
  with the reachable `ip`, the `ports` list (with manifest
  labels), and — for apps that emit one on the boot log —
  a `public_key` the server auto-captures (RustDesk does this).
  Clients connect to `<ip>:<port>` with the app's own
  encryption. The `domain` field in the body is ignored.
- `hybrid` — raw protocol ports AND an HTTP admin UI behind
  Caddy on a subdomain. Response carries BOTH `domain` (admin
  UI) and `connection_info` (raw ports).

Port-conflict guard: fixed protocol ports can only be bound
once per host, so a second `host_ports`/`hybrid` deploy on
the same agent that reuses a port returns `409 CONFLICT`.

Domain handling — match the clientarea behavior across all
callers (API, CLI, MCP). Applies to `http` + `hybrid` modes;
`host_ports` ignores the `domain` field entirely:

- **Leave `domain` empty** → server auto-generates
  `<app-slug>-<6hex>.imprezaapps.com`, creates the Cloudflare
  A record at the agent's public IP, and uses it as the
  deployment hostname. Most common path for new customers.
- **Pass `domain` ending in `.imprezaapps.com`** → server
  ensures a Cloudflare A record exists for that exact name
  (idempotent — reuses if already there).
- **Pass a domain you own** → passthrough; you handle DNS
  (point an A record at the agent's public IP before deploy
  so Let's Encrypt can issue).
**Cold standby exception:** with `standby: true`, no hostname is generated.
Omit `domain`, `onion` and `vars.domain`; only the reviewed Memos 0.31.0 recipe is accepted.

## Request body

- object
  - `standby` boolean: Create a cold standby without a public route. Requires the failover_enabled gate. Only the reviewed Memos 0.31.0 catalog version is supported. Omit domain, onion, and vars.domain. A standby receives no auto-generated hostname and no Tor service. Private onion, customer domains, database bindings and unreviewed recipes are unsupported. Creation does not pair, copy data or switch traffic; use the failover standby workflow.
  - `app_name` string, required: Name of the catalog app (e.g. `vaultwarden`).
  - `agent_id` string, required
  - `app_version` string: Specific version. Defaults to `latest_version` from the catalog.
  - `domain` string: Public hostname the app will respond on (FQDN, no scheme). Optional — leave empty to get an auto-generated `<slug>-<6hex>.imprezaapps.com` subdomain. When set to a `*.imprezaapps.com` name, the server ensures the Cloudflare record exists. For customer-owned apex domains, point the A record at the agent IP before calling so Let's Encrypt can issue. **Uniqueness:** `*.imprezaapps.com` is a shared namespace — first-come-first-served across all accounts. Picking a name already claimed by any live deployment (yours or someone else's) returns `400 INVALID_REQUEST` with a neutral "already in use" message (no enumeration on other accounts). Names are released when the deploy fully uninstalls.
  - `onion` boolean: Also publish a Tor v3 hidden service alongside the clearnet URL.
  - `vars` object: App-specific variables. The exact keys depend on the manifest — call `GET /platform/apps/{name}` to see declared `vars` with their `required` / `default` / `generate` settings.

## Response `201`

Deployment enqueued

## Other responses

- `400`: Validation failed on the request body or query parameters
- `404`: Resource not found
- `409`: Request would violate a unique constraint or state precondition

---

[API](https://skmtc.dev/imprezahost/apis/impreza-host-public-api.md) · [All operations](https://skmtc.dev/imprezahost/apis/impreza-host-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/imprezahost/apis/impreza-host-public-api/revisions/db86511f604f?raw)
