API

Apps API

Browse the one-click app catalog, install an app as a project, manage per-organization custom apps, and read an installed app's curated settings and resolved connection details.

An app is a JSON definition that installs a set of containers as a project in one click — with its images, wiring, generated secrets, curated settings form, and connection card already described. This API covers browsing the catalog, installing, adding your organization's own custom apps, and reading or updating an installed app afterwards.

In the dashboard this is the Apps tab. The definition format is documented in the App catalog JSON reference, and authoring workflow in Add an app.

Base path & auth

Catalog and install paths sit at /api/apps; the two per-project paths are nested under /api/projects/:id. Send a personal access token as a bearer header (Authorization: Bearer <token>), created with openship token create. The dashboard uses your session cookie instead. See the API overview for the full auth model and security/permissions for how tags map to grants.

Self-hosted and cloud

Apps install as normal services projects, so this module is available on both self-hosted and Openship Cloud. For the per-project paths, a cloud project is transparently proxied to Openship Cloud while a local project is served by your own instance.

Endpoints

Method & pathPermissionWhat it does
GET /api/apps/catalogproject:listList the app catalog.
GET /api/apps/catalog/:idproject:listGet one app's full definition by id.
GET /api/apps/catalog/:id/host-fitproject:listCheck a destination against the app's declared minimum resources.
GET /api/apps/customproject:listList this organization's custom (uploaded, unverified) apps.
POST /api/apps/customproject:writeAdd a custom app from an uploaded JSON definition.
DELETE /api/apps/custom/:appIdproject:writeRemove a custom app from this organization's catalog.
POST /api/appsproject:writeInstall an app as a project.
GET /api/projects/:id/app-settingsproject:readGet an installed app's curated settings schema and current values.
PATCH /api/projects/:id/app-settingsproject:writeUpdate an installed app's curated settings.
GET /api/projects/:id/app-connectionproject:writeGet an installed app's resolved connection details.

List the catalog

GET /api/apps/catalog

The runtime catalog is the copy bundled in your build, overlaid by a live fetch of the curated catalog from the Openship repo (refreshed on a 10-minute TTL, stale-while-revalidate), plus your organization's custom apps. A failed fetch or an offline instance simply keeps serving the last-good copy, so the overlay can never break the catalog.

Overlay trust model

The overlay is fetched over HTTPS from Openship's own repository — repo-curated, with no signing and no user uploads in that channel — and every entry is shape-validated before it can drive an install, using the same strict validator as the bundled set. Your organization's custom apps are a separate, always-unverified channel.

Because the overlay can be ahead of your instance, entries carry compatibility flags:

Prop

Type

Each entry also carries hosting ("experimental" marks a stack that runs but is heavy or not production-grade upstream) and, for the few apps that declare one, minResources — the host floor you can check with host-fit before installing. Apps marked unlisted are absent from this list by design: they stay installable by id, reached through another app's wizard.

Get one app

GET /api/apps/catalog/:id

Returns the full definition — services, config fields, endpoints, settings, connection. Curated apps win over custom ones: a custom app can never shadow a verified id (also enforced at upload).

This is config metadata only, never secrets — generated values are minted at install, so nothing sensitive exists to return yet.

The response also carries a draft alongside data: this organization's open, never-deployed draft of the same app, if one exists.

Install adopts an open draft

An install request for the same name adopts that draft rather than starting clean. So a client must render the draft's stored configuration, not the app's defaults — otherwise Install silently changes what the operator set up last time.

Check a destination

GET /api/apps/catalog/:id/host-fit?deployTarget=server&serverId=<id>

Does a machine meet what the app declares in minResources? Ask before installing, so the operator sees the shortfall instead of discovering it from a failed deploy.

Prop

Type

{
  "data": {
    "minResources": { "memoryMb": 8192, "cpuCores": 4 },
    "capacity": { "cpuCores": 2, "memoryMb": 2048, "source": "docker" },
    "fit": {
      "ok": false,
      "memory": { "needed": 8192, "available": 2048 },
      "cpu": { "needed": 4, "available": 2 }
    }
  }
}

Advisory, not the gate

This endpoint never refuses anything — it reports. The enforcement point is deploy preflight, which reads the same declaration through the same comparison and fails a first deploy with HOST_RESOURCES_INSUFFICIENT. Both are described under Host requirements.

An id from another organization, a target of cloud, or a machine that cannot be probed all return fit: { "ok": true } with capacity.source: "unknown" — the same "nothing to enforce" answer, so a client has one shape to handle.

Install an app

POST /api/apps

For a template app this creates the project and starts the deploy. For a flow app it returns the route of that app's bespoke wizard instead of installing anything.

Prop

Type

Each routes entry:

Prop

Type

No routes means no domain

Public hostnames come only from routes. A service with no entry gets no public route — a hostname is never invented on your behalf. Omit routes entirely and the app installs port-only, which is a perfectly good outcome for a database or an internal tool.

curl -X POST https://your-host/api/apps \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "templateId": "ghost",
    "name": "my-blog",
    "routes": [
      { "service": "ghost", "port": 2368, "mode": "custom", "customDomain": "blog.acme.com" }
    ]
  }'

Custom apps

POST   /api/apps/custom
GET    /api/apps/custom
DELETE /api/apps/custom/:appId

POST accepts a full app definition as the request body. The body is gated here only as "must be a JSON object" — the authoritative validation runs in the service and is the same strict check as the curated catalog, shape plus every referential rule.

Four constraints are enforced:

  • Always unverified. verified and available are set by the server, so a file claiming "verified": true is ignored.
  • kind must be template. Flow apps are rejected.
  • The id cannot shadow a built-in app.
  • Validation errors are returned with the offending detail, including a distinct message when the definition targets a newer catalog schemaVersion than the instance supports.

A custom app installs through the normal services pipeline with the same in-container boundary as any project you deploy yourself — no new privilege.

Curated settings

GET   /api/projects/:id/app-settings
PATCH /api/projects/:id/app-settings

GET returns the app's settings schema together with current values, so a client can render the form without knowing the app. PATCH applies a safe env merge — only the keys you send are touched:

Prop

Type

Empty-string semantics differ by field kind, which matters when a form re-submits untouched inputs:

Fieldvalue: "" means
Non-secretClear the override, reverting to the app default
SecretLeave the stored value unchanged

Fields marked requiresRedeploy in the definition take effect only after a redeploy; the response and the dashboard both surface that.

Connection details

GET /api/projects/:id/app-connection

Resolves the app's connection outputs against the running services — public URLs, generated keys, composed connection strings — and returns them ready to use, including any labeled variants such as a public versus internal-network form.

This route requires project:write, deliberately

It returns fully decrypted credentials — service-role JWTs, database passwords embedded in postgres://… URLs — which bypass database row-level security. The env listing can mask secrets at project:read; this surface cannot, so it sits above the read tier on purpose. A read-only grant or a project:read MCP token must not be able to exfiltrate live credentials. Treat the response as secret material.

MCP exposure

Every route in this module is also exposed as an MCP tool, so an AI agent can browse the catalog, install an app, and read its settings and connection details. Tool exposure is an opt-in allowlist — a route becomes a tool only when its spec declares an mcp block — and the module-level hard-deny list covers only tokens, auth, and mcp. apps is not on it.

Every MCP call re-checks the caller's permissions, so the tags in the endpoints table apply unchanged. What that means in practice:

Agent tokenCan do
Read-only (project:list / project:read)Browse the catalog, read an installed app's settings schema
project:writeAlso install apps and read decrypted connection credentials

Scope agent tokens deliberately

Because app-connection is a tool and it returns live credentials, a project:write agent token can read an installed app's database passwords and service-role keys. If you are handing a token to an agent that only needs to inspect things, issue a read-only one.

Errors you might see

400 — install refused

Install validates the app and your routing choices, and returns the reason as the message. Common ones: the app is still marked coming soon (available: false); it needs a newer Openship (requiresUpdate, reported as Requires Openship ≥ X); the app id is unknown; a routes entry names a service the app does not declare, or a port that service does not serve; two routes entries target the same service and port; or mode: "custom" was sent without a customDomain. A missing templateId is rejected the same way.

400 — invalid app definition (custom upload)

The uploaded JSON failed the strict schema, and the message carries the offending detail — a bad enum, a missing required field, or a dangling reference such as a service that no declared service matches. Distinct messages cover the three non-shape rejections: a flow app, an id that shadows a built-in, and a definition targeting a newer catalog schemaVersion than this instance supports.

400 — settings rejected

From PATCH /app-settings: the project is not an app or declares no settings (This app has no configurable settings), a change names a key the app does not define (Unknown setting: service.KEY), a required field was cleared, a value failed its pattern or numeric bounds, or the named service does not exist on the project.

403 — needs Openship Cloud

A routes entry used mode: "free" (a managed *.opsh.io subdomain) on an instance that is not connected to Openship Cloud. These carry a CLOUD_REQUIRED_* code so a client can turn them into a connect prompt rather than a dead end. Use mode: "custom" with your own hostname, or mode: "port" for no public route.

404 — unknown app

GET /api/apps/catalog/:id returns { "error": "Unknown app" } when the id is neither a curated app nor one of your organization's custom apps. List valid ids with GET /api/apps/catalog.

See also

On this page