---
title: Apps | Runware Docs
url: https://runware.ai/docs/serverless/api-apps
description: Create an app, change its configuration, deploy a version, and control its lifecycle.
relatedDocuments:
  - https://runware.ai/docs/serverless/api
  - https://runware.ai/docs/serverless/deploying
  - https://runware.ai/docs/serverless/api-versions
---
## Introduction

An **app** is the unit you create, configure and invoke. Everything else on this API hangs beneath one.

**Creating one names the source it runs**, so the archive goes up before the app exists. [Source uploads](https://runware.ai/docs/serverless/api-source-uploads) covers that flow and the `sourceId` it produces.

> [!NOTE]
> `appId` is chosen by you at creation and cannot be changed afterwards. It is released again once the app it named reaches `deleted`, so a name can be reused by a new app that shares nothing with the old one.

## List apps

GETapi.serverless.runware.ai/v1/apps

Returns a page of the organization's apps. Filters combine with AND. Soft-deleted apps are excluded unless `status=deleted` is requested explicitly. Favorited apps appear before non-favorited apps, with the selected ordering applied within each group. A `cursor` is only valid for the `sort` and filters it was issued under. Reusing one across a different ordering or filter set returns `400`.

### [Request](#list-apps-request)

Query

### [limit](#list-apps-query-limit)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `1`
- **Max**: `100`
- **Default**: `20`

Maximum number of items to return.

### [cursor](#list-apps-query-cursor)

- **Type**: `string`

Opaque pagination cursor returned by a previous call, as `nextCursor` or, on the operations that offer one, `prevCursor`.

### [status](#list-apps-query-status)

- **Type**: `string`

Return only apps in this status.

**Allowed values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [q](#list-apps-query-q)

- **Type**: `string`
- **Min**: `1`
- **Max**: `100`

Case-insensitive substring match against `appName` and `appId`. An app matching either is returned.

### [gpuType](#list-apps-query-gputype)

- **Type**: `string`
- **Min**: `1`
- **Max**: `64`

Return only apps whose worker configuration requests this GPU type. Matched against `configuration.gpuType` only, not `fallbackGpuType`. Must be a code from `GET /v1/gpu-types`. An unknown code returns `422`.

### [sort](#list-apps-query-sort)

- **Type**: `string`
- **Default**: `createdAt`

Ordering for `listApps`. Favorited apps appear before non-favorited apps, and the selected ordering applies within each group. Every ordering is total (ties broken by `appId`), so a page is reproducible and its cursor stable. - `createdAt`: newest first. The default. - `name`: `appName` A–Z, case-insensitive.

**Allowed values**: `createdAt` `name`

### [Response](#list-apps-response)

### [nextCursor](#list-apps-response-nextcursor)

- **Type**: `string (nullable)`

Cursor for the next page. Null when there are no more items.

### [data](#list-apps-response-data)

- **Type**: `object[] (53 properties)`

#### [appId](#list-apps-response-data-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [appName](#list-apps-response-data-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

#### [configuration](#list-apps-response-data-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

##### [id](#list-apps-response-data-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

##### [appId](#list-apps-response-data-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

##### [computeType](#list-apps-response-data-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

##### [gpuType](#list-apps-response-data-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

##### [fallbackGpuType](#list-apps-response-data-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

##### [gpusPerWorker](#list-apps-response-data-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

##### [minWorkers](#list-apps-response-data-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

##### [maxWorkers](#list-apps-response-data-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

##### [minAvailableWorkers](#list-apps-response-data-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

##### [availableWorkersPct](#list-apps-response-data-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

##### [idleTtlSecs](#list-apps-response-data-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

##### [scalingDelaySecs](#list-apps-response-data-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

##### [requestTimeoutSecs](#list-apps-response-data-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

##### [startupTimeoutSecs](#list-apps-response-data-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

##### [createdAt](#list-apps-response-data-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

##### [updatedAt](#list-apps-response-data-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [health](#list-apps-response-data-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

##### [state](#list-apps-response-data-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

##### [reason](#list-apps-response-data-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

##### [since](#list-apps-response-data-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

##### [observedAt](#list-apps-response-data-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

#### [effectiveMaxWorkers](#list-apps-response-data-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

#### [runtime](#list-apps-response-data-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

##### [activeWorkers](#list-apps-response-data-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

##### [provisionedGpuCount](#list-apps-response-data-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

##### [calculatedAt](#list-apps-response-data-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

##### [requests24h](#list-apps-response-data-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

##### [errorRate24h](#list-apps-response-data-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

##### [averageRequestDuration24h](#list-apps-response-data-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

##### [queueDepth](#list-apps-response-data-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

#### [secrets](#list-apps-response-data-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

##### [id](#list-apps-response-data-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

##### [name](#list-apps-response-data-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

##### [type](#list-apps-response-data-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

##### [metadata](#list-apps-response-data-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

##### [createdAt](#list-apps-response-data-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

##### [updatedAt](#list-apps-response-data-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

##### [envVarName](#list-apps-response-data-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

#### [environmentVariables](#list-apps-response-data-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

##### [id](#list-apps-response-data-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

##### [appId](#list-apps-response-data-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

##### [key](#list-apps-response-data-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

##### [value](#list-apps-response-data-environmentvariables-value)

- **Type**: `string`
- **Required**: true

##### [createdAt](#list-apps-response-data-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

##### [updatedAt](#list-apps-response-data-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [status](#list-apps-response-data-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

#### [isFavorite](#list-apps-response-data-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

#### [activeVersionId](#list-apps-response-data-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

#### [createdAt](#list-apps-response-data-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

#### [updatedAt](#list-apps-response-data-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#list-apps-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">400</code></td><td data-rw-cid-pz7tphoj="true">The request was malformed and could not be parsed (e.g. invalid JSON). A well-formed request that fails validation returns <code class="component-Code">422</code> instead.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but semantically invalid (e.g. a missing or out-of-range field). A request that could not be parsed returns <code class="component-Code">400</code>. The <code class="component-Code">errors</code> array carries one entry per offending field.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable</td></tr></tbody></table>

## Create an app

POSTapi.serverless.runware.ai/v1/apps

Creates an app together with its worker configuration, environment variables and endpoints, and records version `1`, the immutable description of what to deploy. The app starts in `initializing`, and what happens next depends on the app source type: - `code` source: the codebase is submitted to the build pipeline. Once the image is built and workers become healthy the app transitions to `active` and `activeVersionId` points at that version. If the build, validation, or rollout fails the app is marked `failed`. - `container` source: the submitted zip (wrapper `Dockerfile` + `container.yaml`) goes through the same build pipeline. The wrapper image is built, published and deployed, so the version carries a `buildId` and the app follows the same lifecycle as a code source. An invalid `container.yaml` rejects the create before any build capacity is spent: `400` where the document could not be parsed at all, `422` where it parsed and broke a rule. `activeVersionId` is null until a rollout completes: a version records what should run, and only a finished deploy says what does. `secrets` attaches organization secrets that already exist. It is the app's initial attachment set, so the first rollout carries their values into the worker. This route does not create a secret. Use `POST /v1/secrets` first. A name that is unknown to the organization, or that is not `active`, returns `404`. A name that collides with a key in `environmentVariables`, a repeated name and a set that goes past the binding limit each return `422`. The whole set is checked before any build capacity is spent.

> [!WARNING]
> Creating an app does not make it callable. The first build has to finish and roll out, and until it does the app stays `initializing` and answers `409`.

### [Request](#create-app-request)

Body

### [appId](#create-app-body-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#create-app-body-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#create-app-body-configuration)

- **Type**: `object (12 properties)`
- **Required**: true

#### [computeType](#create-app-body-configuration-computetype)

- **Type**: `string`
- **Default**: `gpu`

GPU is the only supported compute type. Omitting the field selects GPU. CPU workloads are not supported. A request that names `cpu` is rejected with 422 before a build or deploy starts.

**Allowed values**: `gpu`

#### [gpuType](#create-app-body-configuration-gputype)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `64`

GPU type the workers run on. Required: omitting it (or sending null) is a 422 before a build or deploy starts, because a GPU app with no type is unpinned and the deployer would render NVIDIA defaults. Must match an `id` returned by `GET /v1/gpu-types` that currently has admitted capacity.

#### [fallbackGpuType](#create-app-body-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Max**: `64`

Substitute GPU type used when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. It must differ from `gpuType` and resolve to admitted capacity in the same cluster and runtime class. It must grant `gpusPerWorker`, and declare memory, ephemeral-storage, and CPU limits that can hold the preferred pool's requests. A kubernetes preferred pool cannot name a coordinator substitute. Charged at the substitute's catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type. Omit, send JSON null, or send an empty string for no substitute. Forms bind an unselected dropdown as `""`, which is not a `GpuTypeId`.

#### [gpusPerWorker](#create-app-body-configuration-gpusperworker)

- **Type**: `integer`
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. A worker holds its GPUs as one group the cluster grants indivisibly, so the count is one of the advertised group sizes rather than any number in a range. The value must also be a group size admitted by the cluster backing the chosen `gpuType`: a count above 1 that cluster does not grant is rejected with a 422 naming `/configuration/gpusPerWorker`, since the pod could never be scheduled. A count above 1 is rejected the same way when that type, or the fallback type, has `multiGpuSupport` false. A value above 1 requires an image built after the multi-GPU worker entrypoint. Older images serve a single rank while holding every granted GPU.

**Allowed values**: `1` `2` `4` `8`

#### [minWorkers](#create-app-body-configuration-minworkers)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

#### [maxWorkers](#create-app-body-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#create-app-body-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand. Omit, or send null or 0, for no buffer. Must be lower than `maxWorkers`. The buffer applies only while the queue is non-empty, so it does not stop an idle app scaling to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#create-app-body-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Omit, or send null or 0, for no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty, so it does not stop an idle app scaling to `minWorkers`.

#### [idleTtlSecs](#create-app-body-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [scalingDelaySecs](#create-app-body-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [requestTimeoutSecs](#create-app-body-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only. Omit on a container app to take 60s. A code app may still send or omit this field: omit stores 600s for the console, and neither value is applied to MLflow reads, which keep the platform ceiling. A later `container.yaml` deploy overwrites the live value with that document's `timeouts.requestSeconds`. Bounds the worker, not the synchronous HTTP wait.

#### [startupTimeoutSecs](#create-app-body-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Omit to take the source default: 300s for a container app, 2400s for a code app (the probe budget a code worker already had). A later `container.yaml` deploy overwrites the live value with that document's `timeouts.startupSeconds`.

### [appSource](#create-app-body-appsource)

- **Type**: `object (4 properties)`
- **Required**: true

Write-only. Source for the app's first version. Not returned in the App response. Use the `/builds` endpoints to inspect build status. - `code`: the codebase is submitted to the build pipeline. A new image is built and deployed once ready. - `container`: the submitted zip is built into a wrapper image by the same pipeline and deployed once ready. The version records the built image, not a customer-supplied reference. Either way the version is recorded with the app, and the app transitions to `active` once its first version is ready, or to `failed` if the build, validation, or rollout fails.

#### [type](#create-app-body-appsource-type)

- **Type**: `string`
- **Required**: true

Selects the version creation path. `code` submits customer source code to the Image Build Service. `container` submits a wrapper `Dockerfile` and `container.yaml` for Runware to build into a hosted image.

**Allowed values**: `code` `container`

#### [source](#create-app-body-appsource-source)

- **Type**: `variant (2 properties)`
- **Required**: true

**Format 1: object**:

##### [baseImage](#create-app-body-appsource-source-format-1-baseimage)

- **Type**: `string`
- **Required**: true

Base image of the served image, e.g. `python:3.12-slim`. It must provide `python` 3.12 or newer on its PATH, and the build fails if it does not. The build imports the model file under Python 3.12. If the `.python-version` or the `requires-python` of `pyproject.toml` in the codebase excludes 3.12, the build uses a version that it permits. The Python of the base image must then also be one that they permit, and a `.python-version` sets the oldest one.

##### [requirements](#create-app-body-appsource-source-format-1-requirements)

- **Type**: `string[]`

Additional pip packages to install alongside the codebase.

##### [codebase](#create-app-body-appsource-source-format-1-codebase)

- **Type**: `object (2 properties)`
- **Required**: true

##### [sourceId](#create-app-body-appsource-source-format-1-codebase-sourceid)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

Id of a source published by a completed upload in this organization (`POST /v1/source-uploads`, then `complete`). The archive it names is the zip of the customer's code. A source is immutable and reusable: the same `sourceId` may back any number of apps and versions, each choosing its own `modelFile`.

##### [modelFile](#create-app-body-appsource-source-format-1-codebase-modelfile)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `512`

Path of the MLflow model entry point inside the archive, relative to its root (e.g. `model.py`). Version execution configuration rather than archive identity, so two apps may run one source with different entry points. The build proves the file is in the archive and answers `422` when it is not.

**Format 2: object**:

##### [sourceId](#create-app-body-appsource-source-format-2-sourceid)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

Id of a source published by a completed upload in this organization (`POST /v1/source-uploads`, then `complete`). The archive it names carries a wrapper `Dockerfile` and a `container.yaml` config document at its root, plus any build-context files the Dockerfile copies in. Runware builds the image from it, resolving the Dockerfile's public base images to immutable digests, and hosts the result, so no image reference or pull credential is supplied: a private base image is not supported until a build-time credential mechanism exists. An invalid `container.yaml` rejects the create: `400` where the document could not be parsed, `422` where it parsed and broke a rule, with `errors[]` entries carrying `configPointer` into the document. The endpoint set it declares becomes visible once the first build is ready and deployed. A source is immutable and reusable: the same `sourceId` may back any number of apps and versions in the organization.

### [secrets](#create-app-body-secrets)

- **Type**: `object[] (2 properties)`

Existing organization secrets to attach to this app, with an optional env-var name override per entry. This is the app's initial attachment set, so the first rollout carries the values into the worker. The secret must already exist and be `active`. This route does not create one, and an unknown or inactive name returns `404`. Shape matches `POST /apps/{appId}/secrets` so create and attach share one contract. Each injected name must not collide with a key in `environmentVariables`. See `SecretAttach`. Each secret can also be attached to at most 25 deployments total, shared with every other route that attaches it. An entry that would push a secret past that returns `422`.

#### [secretName](#create-app-body-secrets-secretname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [envVarName](#create-app-body-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Environment variable name the secret is injected as. Omit or null to use `secretName`. The server resolves and stores the final name. Same reserved-name rules as `SecretName` (`422` if reserved). The resolved name must also not collide with a plain environment variable key on this app (`422`).

### [environmentVariables](#create-app-body-environmentvariables)

- **Type**: `object`

Map with environment variables. Keys are the environment variable names, values are the environment variable values. Use the dedicated `/environment-variables` endpoints to change them after the app exists. A code source's build imports the model file with these set, so a variable read at import is there at build time as well as at serve time. Secrets never reach a build. Each key must satisfy `EnvironmentVariableName`. POSIX-style, at most 128 characters. OpenAPI 3.0 cannot constrain map keys, so a bad one is rejected by the server rather than by the schema. Keys must also not collide with a secret's injected env var name on the same app (see `attachAppSecret`).

### [volumes](#create-app-body-volumes)

- **Type**: `object[] (1 property)`
- **Max items**: `30`

Persistent node-local directories bind-mounted through the checkpointer into the sandboxed application. Use these for downloaded weights and caches that must stay outside the checkpointed root filesystem. Paths must be unique and non-overlapping. The set is frozen into each immutable app version.

#### [mountPath](#create-app-body-volumes-mountpath)

- **Type**: `string`
- **Required**: true
- **Min**: `2`
- **Max**: `2048`

Absolute path exposed inside the sandboxed application. The path is the volume's stable identity and is mirrored below the app's node-local data directory. Root, duplicate, and overlapping paths are rejected. Each path component (the text between `/` separators) is at most 255 bytes, the filesystem NAME\_MAX the mirrored node directory must satisfy. `/data/liquid-components` and `/data/liquid-large-data` are reserved by the platform: a path on one, under one (`/data/liquid-components/models`) or over one (`/data`) is rejected with `422`. A path that only shares a string prefix with one, such as `/data/liquid-components-x`, is accepted.

### [Response](#create-app-response)

### [appId](#create-app-response-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#create-app-response-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#create-app-response-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

#### [id](#create-app-response-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [appId](#create-app-response-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [computeType](#create-app-response-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

#### [gpuType](#create-app-response-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

#### [fallbackGpuType](#create-app-response-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

#### [gpusPerWorker](#create-app-response-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

#### [minWorkers](#create-app-response-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

#### [maxWorkers](#create-app-response-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#create-app-response-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#create-app-response-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

#### [idleTtlSecs](#create-app-response-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

#### [scalingDelaySecs](#create-app-response-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

#### [requestTimeoutSecs](#create-app-response-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

#### [startupTimeoutSecs](#create-app-response-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

#### [createdAt](#create-app-response-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#create-app-response-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [health](#create-app-response-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

#### [state](#create-app-response-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

#### [reason](#create-app-response-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

#### [since](#create-app-response-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

#### [observedAt](#create-app-response-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

### [effectiveMaxWorkers](#create-app-response-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

### [runtime](#create-app-response-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

#### [activeWorkers](#create-app-response-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

#### [provisionedGpuCount](#create-app-response-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

#### [calculatedAt](#create-app-response-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

#### [requests24h](#create-app-response-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

#### [errorRate24h](#create-app-response-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

#### [averageRequestDuration24h](#create-app-response-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

#### [queueDepth](#create-app-response-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

### [secrets](#create-app-response-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

#### [id](#create-app-response-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [name](#create-app-response-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [type](#create-app-response-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

#### [metadata](#create-app-response-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

#### [createdAt](#create-app-response-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#create-app-response-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [envVarName](#create-app-response-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

### [environmentVariables](#create-app-response-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

#### [id](#create-app-response-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

#### [appId](#create-app-response-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [key](#create-app-response-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

#### [value](#create-app-response-environmentvariables-value)

- **Type**: `string`
- **Required**: true

#### [createdAt](#create-app-response-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#create-app-response-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [status](#create-app-response-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [isFavorite](#create-app-response-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

### [activeVersionId](#create-app-response-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

### [createdAt](#create-app-response-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [updatedAt](#create-app-response-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#create-app-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">400</code></td><td data-rw-cid-pz7tphoj="true">The request could not be parsed (<code class="component-Code">bad-request</code>), or it parsed and the <code class="component-Code">container.yaml</code> inside its container source could not be (<code class="component-Code">container-config-yaml-invalid</code>). The second carries <code class="component-Code">errors[]</code>. Where the parser can name a field, an entry has a <code class="component-Code">configPointer</code> into <code class="component-Code">container.yaml</code> and a <code class="component-Code">pointer</code> to the archive in the request body. A document that parses and then breaks a rule returns <code class="component-Code">422</code> instead.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">402</code></td><td data-rw-cid-pz7tphoj="true">The organization's credit cannot cover the capacity the request asks for. The problem <code class="component-Code">type</code> is <code class="component-Code">insufficient-credit</code> when the available balance is short, and <code class="component-Code">credit-suspended</code> when a refund took back credit already spent and every allocation is refused until the balance is funded back. Either way <code class="component-Code">shortfall</code> is the amount to add before retrying: the request is unchanged by the refusal and succeeds as sent once the credit is there.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">403</code></td><td data-rw-cid-pz7tphoj="true">Authenticated but not permitted to access this resource</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">404</code></td><td data-rw-cid-pz7tphoj="true">Resource not found</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">409</code></td><td data-rw-cid-pz7tphoj="true">Resource already exists or the request conflicts with its current state</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">413</code></td><td data-rw-cid-pz7tphoj="true">The request body exceeds its size limit: 10 MiB on <code class="component-Code">invoke-sync</code> and <code class="component-Code">invoke-async</code>, whose body carries the endpoint's payload, and 1 MiB on the other operations that answer with this response. <code class="component-Code">detail</code> names the limit in bytes.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">415</code></td><td data-rw-cid-pz7tphoj="true">The request body arrived with a Content-Type this operation does not accept. The body was not decoded, so this is not a schema failure. A body of <code class="component-Code">application/json</code> that cannot be parsed returns <code class="component-Code">400</code>. One that parses and breaks the schema returns <code class="component-Code">422</code>. A body sent with no Content-Type is the same refusal.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but invalid. A field of the request body that broke the schema or a rule is <code class="component-Code">unprocessable-entity</code>. A container source whose archive or <code class="component-Code">container.yaml</code> broke a contract rule carries the rule's own type and <code class="component-Code">errors[]</code>. An entry about a field of <code class="component-Code">container.yaml</code> has a <code class="component-Code">configPointer</code> to that field and a <code class="component-Code">pointer</code> to the archive in the request body. An entry about the archive has neither. A code source's archive rejection is <code class="component-Code">unprocessable-entity</code>. A request that could not be parsed returns <code class="component-Code">400</code>.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable. <code class="component-Code">Retry-After</code> is present when the client should wait before sending the same request again, and absent when the server cannot estimate one.</td></tr></tbody></table>

## Get an app

GETapi.serverless.runware.ai/v1/apps/{appId}

Returns the app the authenticated organization owns under this `appId`. An unknown app and a soft-deleted one both return `404 Not Found`: a deleted app is gone to its owner, and its rows are retained only for billing and audit. To read deleted apps, list them with `status=deleted`.

### [Request](#get-app-request)

Path

### [appId](#get-app-path-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier, unique among the authenticated organization's live apps.

### [Response](#get-app-response)

### [appId](#get-app-response-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#get-app-response-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#get-app-response-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

#### [id](#get-app-response-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [appId](#get-app-response-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [computeType](#get-app-response-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

#### [gpuType](#get-app-response-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

#### [fallbackGpuType](#get-app-response-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

#### [gpusPerWorker](#get-app-response-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

#### [minWorkers](#get-app-response-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

#### [maxWorkers](#get-app-response-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#get-app-response-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#get-app-response-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

#### [idleTtlSecs](#get-app-response-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

#### [scalingDelaySecs](#get-app-response-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

#### [requestTimeoutSecs](#get-app-response-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

#### [startupTimeoutSecs](#get-app-response-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

#### [createdAt](#get-app-response-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#get-app-response-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [health](#get-app-response-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

#### [state](#get-app-response-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

#### [reason](#get-app-response-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

#### [since](#get-app-response-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

#### [observedAt](#get-app-response-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

### [effectiveMaxWorkers](#get-app-response-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

### [runtime](#get-app-response-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

#### [activeWorkers](#get-app-response-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

#### [provisionedGpuCount](#get-app-response-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

#### [calculatedAt](#get-app-response-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

#### [requests24h](#get-app-response-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

#### [errorRate24h](#get-app-response-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

#### [averageRequestDuration24h](#get-app-response-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

#### [queueDepth](#get-app-response-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

### [secrets](#get-app-response-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

#### [id](#get-app-response-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [name](#get-app-response-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [type](#get-app-response-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

#### [metadata](#get-app-response-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

#### [createdAt](#get-app-response-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#get-app-response-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [envVarName](#get-app-response-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

### [environmentVariables](#get-app-response-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

#### [id](#get-app-response-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

#### [appId](#get-app-response-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [key](#get-app-response-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

#### [value](#get-app-response-environmentvariables-value)

- **Type**: `string`
- **Required**: true

#### [createdAt](#get-app-response-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#get-app-response-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [status](#get-app-response-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [isFavorite](#get-app-response-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

### [activeVersionId](#get-app-response-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

### [createdAt](#get-app-response-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [updatedAt](#get-app-response-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#get-app-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">404</code></td><td data-rw-cid-pz7tphoj="true">Resource not found</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but semantically invalid (e.g. a missing or out-of-range field). A request that could not be parsed returns <code class="component-Code">400</code>. The <code class="component-Code">errors</code> array carries one entry per offending field.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable</td></tr></tbody></table>

## Update an app

PATCHapi.serverless.runware.ai/v1/apps/{appId}

Patches one or more aspects of an app in place. All fields are optional. Omitted fields are left unchanged. Valid in any non-`deleted` status, including `stopped` (changes apply on `resume`). Lifecycle transitions use the dedicated `deploy`, `stop`, `resume`, and `delete` operations. A configuration or `environmentVariables` change records a new version with the same image. If that image is deployable, the update pins it as `activeVersionId` and rolls the workload when the app is `active` or `initializing`. A `failed` app is moved to `initializing` and rolled, the same as `POST /deploy`. If the image is not deployable, the version is recorded and `activeVersionId` is left unchanged. If the roll fails, `activeVersionId` is restored and the previous configuration keeps serving. A name-only change records a version and does not pin. A `stopped` or `stopping` app pins the version and rolls it on `resume`. A configuration, `environmentVariables`, or `appSource` change while a create or resume rollout is already in progress returns `409 Conflict`, as does one to a `failed` app while the workers of its failed rollout are still stopping. A name-only or `secrets`\-only change does not. `appSource` starts a build and records version N+1 with a new image tag. The deploy queue carries the build-then-deploy tail. `activeVersionId` moves only when that rollout completes. A builder rejection (400 where a container document's parser refused it, 422 where it parsed and broke a rule) leaves the app on its current version and writes no version row and no build row. After the builder accepts, version N+1 is recorded even if a concurrent secret deactivation, an env/secret collision, or the 25-deployment attachment ceiling below prevents this request's env/secrets overlay. In that case the previous environmentVariables and attachment set stay in place and are what the new version snapshots, and the `422` the standalone case below returns does not apply here. The request still succeeds. `environmentVariables` replaces the whole set: a key absent from the map is deleted, and a null value omits that key from the new set. The resolved map is snapshotted onto the new version. On a code source, the build `appSource` starts imports the model file with that map, or with the app's current set when the request carries none, so a variable read at import is there at build time too. Secrets never reach a build. `secrets` replaces the whole attachment set. An attachment absent from the array is detached. Injected names must not collide with a plain environment variable on the app. The combined set of plain variables and attachments is capped at 100, and each individual secret can be attached to at most 25 deployments total. An entry that would push a secret past that returns `422`. This is a control-plane record only. Secret values do not reach a pod, and the version snapshot carries no secrets, so a secrets-only change does not roll the workload. Endpoints are not a field of this contract: the set belongs to the app source, so it changes only when a new version with a new source builds and deploys.

> [!NOTE]
> An update records a version even when it changes nothing but configuration. That version carries the previous image forward, so a scaling change costs a version and not a build.

### [Request](#update-app-request)

Path

### [appId](#update-app-path-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier, unique among the authenticated organization's live apps.

Body

### [appName](#update-app-body-appname)

- **Type**: `string`
- **Min**: `1`

Mutable display name. Does not affect app identity or routing. Omit to leave unchanged. An explicit blank or padded value is rejected, not treated as a clear.

### [configuration](#update-app-body-configuration)

- **Type**: `object (11 properties)`

Partial worker configuration. Any field present overwrites the live value. Omitted fields are left unchanged. Clearing a nullable live field (setting it to null) is not supported. Omit the field to leave it unchanged. `fallbackGpuType` is the exception: send an empty string to clear it. `computeType` is create-time only and cannot be patched. Changing `gpuType` affects only newly created workers.

#### [gpuType](#update-app-body-configuration-gputype)

- **Type**: `string`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Omit to leave unchanged. Rejected with a 422 when no capacity is currently offered for the type (it does not appear in `GET /v1/gpu-types`).

#### [fallbackGpuType](#update-app-body-configuration-fallbackgputype)

- **Type**: `string`
- **Max**: `64`

Substitute GPU type, used when the preferred pool has no recorded inventory or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. Omit to leave unchanged. Send an empty string to clear. Forms bind an unselected dropdown as `""`. JSON null is rejected: this field is not nullable, so a client that meant to clear must send `""` rather than null. A non-empty value must differ from `gpuType` and resolve to admitted capacity in the same cluster and runtime class. It must grant `gpusPerWorker`, and declare memory, ephemeral-storage, and CPU limits that can hold the preferred pool's requests. A kubernetes preferred pool cannot name a coordinator substitute. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. Charged at the substitute's catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type.

#### [gpusPerWorker](#update-app-body-configuration-gpusperworker)

- **Type**: `integer`
- **Format**: `int32`

GPUs granted to one worker pod. A worker holds its GPUs as one group the cluster grants indivisibly, so the count is one of the advertised group sizes rather than any number in a range. The value must also be a group size admitted by the cluster backing the chosen `gpuType`: a count above 1 that cluster does not grant is rejected with a 422 naming `/configuration/gpusPerWorker`, since the pod could never be scheduled. A count above 1 is rejected the same way when that type, or the fallback type, has `multiGpuSupport` false. A value above 1 requires an image built after the multi-GPU worker entrypoint. Older images serve a single rank while holding every granted GPU.

**Allowed values**: `1` `2` `4` `8`

#### [minWorkers](#update-app-body-configuration-minworkers)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `0`

#### [maxWorkers](#update-app-body-configuration-maxworkers)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#update-app-body-configuration-minavailableworkers)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand. Omit to leave unchanged. Send 0 to remove the buffer. Null is refused, because omitting a field and clearing it mean different things here. Must be lower than the resolved `maxWorkers`.

#### [availableWorkersPct](#update-app-body-configuration-availableworkerspct)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Omit to leave unchanged. Send 0 to remove the buffer. Null is refused, because omitting a field and clearing it mean different things here.

#### [idleTtlSecs](#update-app-body-configuration-idlettlsecs)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `1`

#### [scalingDelaySecs](#update-app-body-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `1`

#### [requestTimeoutSecs](#update-app-body-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Omit to leave unchanged. Applied to container workers only. A code app stores the new value but keeps the platform MLflow read ceiling.

#### [startupTimeoutSecs](#update-app-body-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

### [appSource](#update-app-body-appsource)

- **Type**: `object (4 properties)`

Write-only. New source to build and deploy. Not returned in the App response. Use the `/builds` endpoints to inspect build status. Triggers a build (for `code` sources) or validates `container.yaml` then builds (for `container` sources). On accept the resulting version is recorded with a new image tag and rolled through the deploy queue. A builder rejection leaves the app on the previous version and writes no version or build row. After accept, a concurrent secret deactivation or env/secret collision leaves version N+1 recorded and the previous attachment set in place. `activeVersionId` moves only when that rollout completes. Not valid on a `stopped` or `stopping` app: there is nothing to roll the new version onto, and `resume` rolls the pinned one, so supplying it in those statuses returns `409 Conflict`.

#### [type](#update-app-body-appsource-type)

- **Type**: `string`
- **Required**: true

Selects the version creation path. `code` submits customer source code to the Image Build Service. `container` submits a wrapper `Dockerfile` and `container.yaml` for Runware to build into a hosted image.

**Allowed values**: `code` `container`

#### [source](#update-app-body-appsource-source)

- **Type**: `variant (2 properties)`
- **Required**: true

**Format 1: object**:

##### [baseImage](#update-app-body-appsource-source-format-1-baseimage)

- **Type**: `string`
- **Required**: true

Base image of the served image, e.g. `python:3.12-slim`. It must provide `python` 3.12 or newer on its PATH, and the build fails if it does not. The build imports the model file under Python 3.12. If the `.python-version` or the `requires-python` of `pyproject.toml` in the codebase excludes 3.12, the build uses a version that it permits. The Python of the base image must then also be one that they permit, and a `.python-version` sets the oldest one.

##### [requirements](#update-app-body-appsource-source-format-1-requirements)

- **Type**: `string[]`

Additional pip packages to install alongside the codebase.

##### [codebase](#update-app-body-appsource-source-format-1-codebase)

- **Type**: `object (2 properties)`
- **Required**: true

##### [sourceId](#update-app-body-appsource-source-format-1-codebase-sourceid)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

Id of a source published by a completed upload in this organization (`POST /v1/source-uploads`, then `complete`). The archive it names is the zip of the customer's code. A source is immutable and reusable: the same `sourceId` may back any number of apps and versions, each choosing its own `modelFile`.

##### [modelFile](#update-app-body-appsource-source-format-1-codebase-modelfile)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `512`

Path of the MLflow model entry point inside the archive, relative to its root (e.g. `model.py`). Version execution configuration rather than archive identity, so two apps may run one source with different entry points. The build proves the file is in the archive and answers `422` when it is not.

**Format 2: object**:

##### [sourceId](#update-app-body-appsource-source-format-2-sourceid)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

Id of a source published by a completed upload in this organization (`POST /v1/source-uploads`, then `complete`). The archive it names carries a wrapper `Dockerfile` and a `container.yaml` config document at its root, plus any build-context files the Dockerfile copies in. Runware builds the image from it, resolving the Dockerfile's public base images to immutable digests, and hosts the result, so no image reference or pull credential is supplied: a private base image is not supported until a build-time credential mechanism exists. An invalid `container.yaml` rejects the create: `400` where the document could not be parsed, `422` where it parsed and broke a rule, with `errors[]` entries carrying `configPointer` into the document. The endpoint set it declares becomes visible once the first build is ready and deployed. A source is immutable and reusable: the same `sourceId` may back any number of apps and versions in the organization.

### [secrets](#update-app-body-secrets)

- **Type**: `object[] (2 properties)`
- **Max items**: `100`

Replaces the app's secret attachments. Same `SecretAttach` shape as create and `POST /apps/{appId}/secrets`. An attachment absent from the array is detached. Injected names must not collide with a plain environment variable on the app. Control-plane record only. Secret values do not reach a pod, and the version snapshot carries no secrets, so this field does not roll the workload. An app holds at most 100 environment bindings in total. This array cannot exceed that ceiling on its own. Each individual secret can also be attached to at most 25 deployments total, shared with every other route that attaches it.

#### [secretName](#update-app-body-secrets-secretname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [envVarName](#update-app-body-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Environment variable name the secret is injected as. Omit or null to use `secretName`. The server resolves and stores the final name. Same reserved-name rules as `SecretName` (`422` if reserved). The resolved name must also not collide with a plain environment variable key on this app (`422`).

### [environmentVariables](#update-app-body-environmentvariables)

- **Type**: `object`

Replaces the app's environment variables. Keys are the variable names, values are the values. A key absent from the map is deleted. A null value omits that key from the new set. The resolved map is snapshotted onto the version this update records, so a deploy applies it. When the copied image is deployable the update pins and rolls, the same as a configuration change.

### [Response](#update-app-response)

### [appId](#update-app-response-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#update-app-response-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#update-app-response-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

#### [id](#update-app-response-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [appId](#update-app-response-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [computeType](#update-app-response-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

#### [gpuType](#update-app-response-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

#### [fallbackGpuType](#update-app-response-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

#### [gpusPerWorker](#update-app-response-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

#### [minWorkers](#update-app-response-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

#### [maxWorkers](#update-app-response-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#update-app-response-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#update-app-response-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

#### [idleTtlSecs](#update-app-response-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

#### [scalingDelaySecs](#update-app-response-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

#### [requestTimeoutSecs](#update-app-response-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

#### [startupTimeoutSecs](#update-app-response-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

#### [createdAt](#update-app-response-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#update-app-response-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [health](#update-app-response-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

#### [state](#update-app-response-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

#### [reason](#update-app-response-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

#### [since](#update-app-response-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

#### [observedAt](#update-app-response-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

### [effectiveMaxWorkers](#update-app-response-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

### [runtime](#update-app-response-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

#### [activeWorkers](#update-app-response-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

#### [provisionedGpuCount](#update-app-response-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

#### [calculatedAt](#update-app-response-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

#### [requests24h](#update-app-response-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

#### [errorRate24h](#update-app-response-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

#### [averageRequestDuration24h](#update-app-response-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

#### [queueDepth](#update-app-response-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

### [secrets](#update-app-response-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

#### [id](#update-app-response-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [name](#update-app-response-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [type](#update-app-response-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

#### [metadata](#update-app-response-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

#### [createdAt](#update-app-response-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#update-app-response-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [envVarName](#update-app-response-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

### [environmentVariables](#update-app-response-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

#### [id](#update-app-response-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

#### [appId](#update-app-response-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [key](#update-app-response-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

#### [value](#update-app-response-environmentvariables-value)

- **Type**: `string`
- **Required**: true

#### [createdAt](#update-app-response-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#update-app-response-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [status](#update-app-response-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [isFavorite](#update-app-response-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

### [activeVersionId](#update-app-response-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

### [createdAt](#update-app-response-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [updatedAt](#update-app-response-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#update-app-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">400</code></td><td data-rw-cid-pz7tphoj="true">The request could not be parsed (<code class="component-Code">bad-request</code>), or it parsed and the <code class="component-Code">container.yaml</code> inside its container source could not be (<code class="component-Code">container-config-yaml-invalid</code>). The second carries <code class="component-Code">errors[]</code>. Where the parser can name a field, an entry has a <code class="component-Code">configPointer</code> into <code class="component-Code">container.yaml</code> and a <code class="component-Code">pointer</code> to the archive in the request body. A document that parses and then breaks a rule returns <code class="component-Code">422</code> instead.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">402</code></td><td data-rw-cid-pz7tphoj="true">The organization's credit cannot cover the capacity the request asks for. The problem <code class="component-Code">type</code> is <code class="component-Code">insufficient-credit</code> when the available balance is short, and <code class="component-Code">credit-suspended</code> when a refund took back credit already spent and every allocation is refused until the balance is funded back. Either way <code class="component-Code">shortfall</code> is the amount to add before retrying: the request is unchanged by the refusal and succeeds as sent once the credit is there.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">403</code></td><td data-rw-cid-pz7tphoj="true">Authenticated but not permitted to access this resource</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">404</code></td><td data-rw-cid-pz7tphoj="true">Resource not found</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">409</code></td><td data-rw-cid-pz7tphoj="true">Resource already exists or the request conflicts with its current state</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">413</code></td><td data-rw-cid-pz7tphoj="true">The request body exceeds its size limit: 10 MiB on <code class="component-Code">invoke-sync</code> and <code class="component-Code">invoke-async</code>, whose body carries the endpoint's payload, and 1 MiB on the other operations that answer with this response. <code class="component-Code">detail</code> names the limit in bytes.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">415</code></td><td data-rw-cid-pz7tphoj="true">The request body arrived with a Content-Type this operation does not accept. The body was not decoded, so this is not a schema failure. A body of <code class="component-Code">application/json</code> that cannot be parsed returns <code class="component-Code">400</code>. One that parses and breaks the schema returns <code class="component-Code">422</code>. A body sent with no Content-Type is the same refusal.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but invalid. A field of the request body that broke the schema or a rule is <code class="component-Code">unprocessable-entity</code>. A container source whose archive or <code class="component-Code">container.yaml</code> broke a contract rule carries the rule's own type and <code class="component-Code">errors[]</code>. An entry about a field of <code class="component-Code">container.yaml</code> has a <code class="component-Code">configPointer</code> to that field and a <code class="component-Code">pointer</code> to the archive in the request body. An entry about the archive has neither. A code source's archive rejection is <code class="component-Code">unprocessable-entity</code>. A request that could not be parsed returns <code class="component-Code">400</code>.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable. <code class="component-Code">Retry-After</code> is present when the client should wait before sending the same request again, and absent when the server cannot estimate one.</td></tr></tbody></table>

## Delete an app

DELETEapi.serverless.runware.ai/v1/apps/{appId}

Soft delete. Sets `status = deleting` and returns `202` once that intent is persisted. Router removal, canceling in-progress builds, and worker drain (`draining → stopping → stopped`) are performed asynchronously by the deployer/Scaler. `status` becomes `deleted` once all workers stop. All rows are retained for billing finalization, audit, and usage history. Idempotent if the app is already `deleting`. The `appId` is released once `status` reaches `deleted`, and not before: while the app is `deleting` its workload is still being torn down and the name stays taken. A new app created under a released name is a new app and inherits nothing, no version, no build, no event history, and no workers.

### [Request](#delete-app-request)

Path

### [appId](#delete-app-path-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier, unique among the authenticated organization's live apps.

### [Response](#delete-app-response)

### [appId](#delete-app-response-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#delete-app-response-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#delete-app-response-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

#### [id](#delete-app-response-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [appId](#delete-app-response-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [computeType](#delete-app-response-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

#### [gpuType](#delete-app-response-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

#### [fallbackGpuType](#delete-app-response-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

#### [gpusPerWorker](#delete-app-response-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

#### [minWorkers](#delete-app-response-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

#### [maxWorkers](#delete-app-response-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#delete-app-response-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#delete-app-response-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

#### [idleTtlSecs](#delete-app-response-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

#### [scalingDelaySecs](#delete-app-response-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

#### [requestTimeoutSecs](#delete-app-response-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

#### [startupTimeoutSecs](#delete-app-response-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

#### [createdAt](#delete-app-response-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#delete-app-response-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [health](#delete-app-response-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

#### [state](#delete-app-response-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

#### [reason](#delete-app-response-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

#### [since](#delete-app-response-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

#### [observedAt](#delete-app-response-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

### [effectiveMaxWorkers](#delete-app-response-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

### [runtime](#delete-app-response-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

#### [activeWorkers](#delete-app-response-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

#### [provisionedGpuCount](#delete-app-response-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

#### [calculatedAt](#delete-app-response-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

#### [requests24h](#delete-app-response-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

#### [errorRate24h](#delete-app-response-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

#### [averageRequestDuration24h](#delete-app-response-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

#### [queueDepth](#delete-app-response-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

### [secrets](#delete-app-response-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

#### [id](#delete-app-response-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [name](#delete-app-response-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [type](#delete-app-response-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

#### [metadata](#delete-app-response-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

#### [createdAt](#delete-app-response-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#delete-app-response-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [envVarName](#delete-app-response-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

### [environmentVariables](#delete-app-response-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

#### [id](#delete-app-response-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

#### [appId](#delete-app-response-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [key](#delete-app-response-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

#### [value](#delete-app-response-environmentvariables-value)

- **Type**: `string`
- **Required**: true

#### [createdAt](#delete-app-response-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#delete-app-response-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [status](#delete-app-response-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [isFavorite](#delete-app-response-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

### [activeVersionId](#delete-app-response-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

### [createdAt](#delete-app-response-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [updatedAt](#delete-app-response-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#delete-app-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">404</code></td><td data-rw-cid-pz7tphoj="true">Resource not found</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but semantically invalid (e.g. a missing or out-of-range field). A request that could not be parsed returns <code class="component-Code">400</code>. The <code class="component-Code">errors</code> array carries one entry per offending field.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable</td></tr></tbody></table>

## Deploy a version

POSTapi.serverless.runware.ai/v1/apps/{appId}/deploy

Activates a `ready` version by number, setting `activeVersionId` and returning `202` once that intent is persisted. Worker rollout, routing switch, and canceling in-progress builds (`superseded`) are performed asynchronously by the deployer/Scaler. Permitted in any addressable status, including `initializing` and `failed`. To roll back, supply an older `versionNumber`. The operation is identical to a forward deploy. No new version is created and no rebuild happens: the version's existing image is re-applied. Re-deploying the currently active version is permitted and re-applies it. A deploy to a `stopped` or `stopping` app records the version and rolls no workload, because no workers are running: the `202` does not imply a rollout there. The recorded version is the one applied when the app resumes. If the roll of a live app fails, `activeVersionId` is restored to the version that kept serving, so the field keeps naming the running image. **Rollout** (deployer/Scaler): the platform starts workers on the target version, waits for at least one to become healthy, switches task routing to the new version, then drains old-version workers gracefully. Old workers are given a fixed, platform-managed grace period to finish in-flight tasks before being force-terminated. If new workers fail to become healthy, old workers are not drained and the app continues on the previous version. **Readiness wait**: a version with `minWorkers` above zero becomes active only when that many of its workers can serve. The wait starts when the workload is applied, not when this request arrives. It lasts 10 minutes by default. It continues after that, up to 1 hour, while enough of its workers are still pulling or loading their image to reach `minWorkers`. A first pull of a large image on a node can take this long. If the wait ends first, the rollout fails. A first rollout then moves the app to `failed` and the platform stops its workers. To recover, deploy a `ready` version again with this operation once those workers have stopped. Errors: - Deploy to a `deleting` app returns `409 Conflict` - `versionNumber` not found or not `ready` returns `409 Conflict` - A deploy while a rollout of this app is still in flight returns `409 Conflict`. One app rolls to one version at a time. Retry once it completes. - A deploy to a `failed` app while the workers of its failed rollout are still stopping returns `409 Conflict`. Retry once they have stopped. - Deploy to a non-existent or `deleted` app returns `404 Not Found`

### [Request](#deploy-version-request)

Path

### [appId](#deploy-version-path-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier, unique among the authenticated organization's live apps.

Body

### [versionNumber](#deploy-version-body-versionnumber)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Version number to deploy. Must reference a `ready` version on this app. Any in-progress or queued builds are canceled. Use an older version number to roll back.

### [Response](#deploy-version-response)

### [appId](#deploy-version-response-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#deploy-version-response-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#deploy-version-response-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

#### [id](#deploy-version-response-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [appId](#deploy-version-response-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [computeType](#deploy-version-response-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

#### [gpuType](#deploy-version-response-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

#### [fallbackGpuType](#deploy-version-response-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

#### [gpusPerWorker](#deploy-version-response-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

#### [minWorkers](#deploy-version-response-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

#### [maxWorkers](#deploy-version-response-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#deploy-version-response-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#deploy-version-response-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

#### [idleTtlSecs](#deploy-version-response-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

#### [scalingDelaySecs](#deploy-version-response-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

#### [requestTimeoutSecs](#deploy-version-response-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

#### [startupTimeoutSecs](#deploy-version-response-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

#### [createdAt](#deploy-version-response-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#deploy-version-response-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [health](#deploy-version-response-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

#### [state](#deploy-version-response-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

#### [reason](#deploy-version-response-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

#### [since](#deploy-version-response-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

#### [observedAt](#deploy-version-response-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

### [effectiveMaxWorkers](#deploy-version-response-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

### [runtime](#deploy-version-response-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

#### [activeWorkers](#deploy-version-response-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

#### [provisionedGpuCount](#deploy-version-response-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

#### [calculatedAt](#deploy-version-response-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

#### [requests24h](#deploy-version-response-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

#### [errorRate24h](#deploy-version-response-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

#### [averageRequestDuration24h](#deploy-version-response-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

#### [queueDepth](#deploy-version-response-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

### [secrets](#deploy-version-response-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

#### [id](#deploy-version-response-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [name](#deploy-version-response-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [type](#deploy-version-response-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

#### [metadata](#deploy-version-response-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

#### [createdAt](#deploy-version-response-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#deploy-version-response-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [envVarName](#deploy-version-response-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

### [environmentVariables](#deploy-version-response-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

#### [id](#deploy-version-response-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

#### [appId](#deploy-version-response-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [key](#deploy-version-response-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

#### [value](#deploy-version-response-environmentvariables-value)

- **Type**: `string`
- **Required**: true

#### [createdAt](#deploy-version-response-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#deploy-version-response-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [status](#deploy-version-response-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [isFavorite](#deploy-version-response-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

### [activeVersionId](#deploy-version-response-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

### [createdAt](#deploy-version-response-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [updatedAt](#deploy-version-response-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#deploy-version-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">400</code></td><td data-rw-cid-pz7tphoj="true">The request was malformed and could not be parsed (e.g. invalid JSON). A well-formed request that fails validation returns <code class="component-Code">422</code> instead.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">402</code></td><td data-rw-cid-pz7tphoj="true">The organization's credit cannot cover the capacity the request asks for. The problem <code class="component-Code">type</code> is <code class="component-Code">insufficient-credit</code> when the available balance is short, and <code class="component-Code">credit-suspended</code> when a refund took back credit already spent and every allocation is refused until the balance is funded back. Either way <code class="component-Code">shortfall</code> is the amount to add before retrying: the request is unchanged by the refusal and succeeds as sent once the credit is there.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">403</code></td><td data-rw-cid-pz7tphoj="true">Authenticated but not permitted to access this resource</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">404</code></td><td data-rw-cid-pz7tphoj="true">Resource not found</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">409</code></td><td data-rw-cid-pz7tphoj="true">Resource already exists or the request conflicts with its current state</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">413</code></td><td data-rw-cid-pz7tphoj="true">The request body exceeds its size limit: 10 MiB on <code class="component-Code">invoke-sync</code> and <code class="component-Code">invoke-async</code>, whose body carries the endpoint's payload, and 1 MiB on the other operations that answer with this response. <code class="component-Code">detail</code> names the limit in bytes.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">415</code></td><td data-rw-cid-pz7tphoj="true">The request body arrived with a Content-Type this operation does not accept. The body was not decoded, so this is not a schema failure. A body of <code class="component-Code">application/json</code> that cannot be parsed returns <code class="component-Code">400</code>. One that parses and breaks the schema returns <code class="component-Code">422</code>. A body sent with no Content-Type is the same refusal.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but semantically invalid (e.g. a missing or out-of-range field). A request that could not be parsed returns <code class="component-Code">400</code>. The <code class="component-Code">errors</code> array carries one entry per offending field.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable</td></tr></tbody></table>

## Stop an app

POSTapi.serverless.runware.ai/v1/apps/{appId}/stop

Moves the app to `stopping` and returns `202` once that intent is persisted. Scale-to-zero and worker drain are performed asynchronously by the Scaler. `status` becomes `stopped` once all workers drain. In-flight tasks have a fixed, platform-managed grace period to complete. Workers that exceed it are force-terminated and their tasks return to the queue per delivery guarantees. New task submissions remain accepted while `stopping`. After the app reaches `stopped`, submissions return `409 Conflict`. Precondition: `status` is `active` or `failed`. The platform already stops the workers of a `failed` app's rollout. Stopping the app takes it to `stopped` once they have.

### [Request](#stop-app-request)

Path

### [appId](#stop-app-path-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier, unique among the authenticated organization's live apps.

### [Response](#stop-app-response)

### [appId](#stop-app-response-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#stop-app-response-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#stop-app-response-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

#### [id](#stop-app-response-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [appId](#stop-app-response-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [computeType](#stop-app-response-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

#### [gpuType](#stop-app-response-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

#### [fallbackGpuType](#stop-app-response-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

#### [gpusPerWorker](#stop-app-response-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

#### [minWorkers](#stop-app-response-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

#### [maxWorkers](#stop-app-response-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#stop-app-response-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#stop-app-response-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

#### [idleTtlSecs](#stop-app-response-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

#### [scalingDelaySecs](#stop-app-response-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

#### [requestTimeoutSecs](#stop-app-response-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

#### [startupTimeoutSecs](#stop-app-response-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

#### [createdAt](#stop-app-response-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#stop-app-response-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [health](#stop-app-response-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

#### [state](#stop-app-response-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

#### [reason](#stop-app-response-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

#### [since](#stop-app-response-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

#### [observedAt](#stop-app-response-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

### [effectiveMaxWorkers](#stop-app-response-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

### [runtime](#stop-app-response-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

#### [activeWorkers](#stop-app-response-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

#### [provisionedGpuCount](#stop-app-response-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

#### [calculatedAt](#stop-app-response-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

#### [requests24h](#stop-app-response-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

#### [errorRate24h](#stop-app-response-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

#### [averageRequestDuration24h](#stop-app-response-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

#### [queueDepth](#stop-app-response-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

### [secrets](#stop-app-response-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

#### [id](#stop-app-response-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [name](#stop-app-response-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [type](#stop-app-response-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

#### [metadata](#stop-app-response-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

#### [createdAt](#stop-app-response-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#stop-app-response-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [envVarName](#stop-app-response-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

### [environmentVariables](#stop-app-response-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

#### [id](#stop-app-response-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

#### [appId](#stop-app-response-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [key](#stop-app-response-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

#### [value](#stop-app-response-environmentvariables-value)

- **Type**: `string`
- **Required**: true

#### [createdAt](#stop-app-response-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#stop-app-response-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [status](#stop-app-response-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [isFavorite](#stop-app-response-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

### [activeVersionId](#stop-app-response-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

### [createdAt](#stop-app-response-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [updatedAt](#stop-app-response-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#stop-app-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">404</code></td><td data-rw-cid-pz7tphoj="true">Resource not found</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">409</code></td><td data-rw-cid-pz7tphoj="true">Resource already exists or the request conflicts with its current state</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but semantically invalid (e.g. a missing or out-of-range field). A request that could not be parsed returns <code class="component-Code">400</code>. The <code class="component-Code">errors</code> array carries one entry per offending field.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable</td></tr></tbody></table>

## Resume an app

POSTapi.serverless.runware.ai/v1/apps/{appId}/resume

Moves the app to `initializing` and returns `202` once that intent is persisted. The Scaler then starts workers and sets the app to `active`. Tasks that remained queued when the app stopped are consumed as workers come online. Precondition: `status = stopped`.

### [Request](#resume-app-request)

Path

### [appId](#resume-app-path-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier, unique among the authenticated organization's live apps.

### [Response](#resume-app-response)

### [appId](#resume-app-response-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#resume-app-response-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#resume-app-response-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

#### [id](#resume-app-response-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [appId](#resume-app-response-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [computeType](#resume-app-response-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

#### [gpuType](#resume-app-response-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

#### [fallbackGpuType](#resume-app-response-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

#### [gpusPerWorker](#resume-app-response-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

#### [minWorkers](#resume-app-response-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

#### [maxWorkers](#resume-app-response-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#resume-app-response-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#resume-app-response-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

#### [idleTtlSecs](#resume-app-response-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

#### [scalingDelaySecs](#resume-app-response-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

#### [requestTimeoutSecs](#resume-app-response-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

#### [startupTimeoutSecs](#resume-app-response-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

#### [createdAt](#resume-app-response-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#resume-app-response-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [health](#resume-app-response-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

#### [state](#resume-app-response-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

#### [reason](#resume-app-response-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

#### [since](#resume-app-response-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

#### [observedAt](#resume-app-response-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

### [effectiveMaxWorkers](#resume-app-response-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

### [runtime](#resume-app-response-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

#### [activeWorkers](#resume-app-response-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

#### [provisionedGpuCount](#resume-app-response-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

#### [calculatedAt](#resume-app-response-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

#### [requests24h](#resume-app-response-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

#### [errorRate24h](#resume-app-response-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

#### [averageRequestDuration24h](#resume-app-response-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

#### [queueDepth](#resume-app-response-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

### [secrets](#resume-app-response-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

#### [id](#resume-app-response-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [name](#resume-app-response-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [type](#resume-app-response-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

#### [metadata](#resume-app-response-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

#### [createdAt](#resume-app-response-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#resume-app-response-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [envVarName](#resume-app-response-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

### [environmentVariables](#resume-app-response-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

#### [id](#resume-app-response-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

#### [appId](#resume-app-response-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [key](#resume-app-response-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

#### [value](#resume-app-response-environmentvariables-value)

- **Type**: `string`
- **Required**: true

#### [createdAt](#resume-app-response-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#resume-app-response-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [status](#resume-app-response-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [isFavorite](#resume-app-response-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

### [activeVersionId](#resume-app-response-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

### [createdAt](#resume-app-response-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [updatedAt](#resume-app-response-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#resume-app-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">402</code></td><td data-rw-cid-pz7tphoj="true">The organization's credit cannot cover the capacity the request asks for. The problem <code class="component-Code">type</code> is <code class="component-Code">insufficient-credit</code> when the available balance is short, and <code class="component-Code">credit-suspended</code> when a refund took back credit already spent and every allocation is refused until the balance is funded back. Either way <code class="component-Code">shortfall</code> is the amount to add before retrying: the request is unchanged by the refusal and succeeds as sent once the credit is there.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">403</code></td><td data-rw-cid-pz7tphoj="true">Authenticated but not permitted to access this resource</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">404</code></td><td data-rw-cid-pz7tphoj="true">Resource not found</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">409</code></td><td data-rw-cid-pz7tphoj="true">Resource already exists or the request conflicts with its current state</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but semantically invalid (e.g. a missing or out-of-range field). A request that could not be parsed returns <code class="component-Code">400</code>. The <code class="component-Code">errors</code> array carries one entry per offending field.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable</td></tr></tbody></table>

## Favorite an app

PUTapi.serverless.runware.ai/v1/apps/{appId}/favorite

Pins the app as a favorite for the authenticated organization so the console can surface it in a Favorites section. Idempotent: favoriting an already-favorited app succeeds and returns the app with `isFavorite: true`. Apps in `deleting` or `deleted` status cannot be favorited (`404`). Soft-delete clears any existing pin when status becomes `deleting`.

### [Request](#favorite-app-request)

Path

### [appId](#favorite-app-path-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier, unique among the authenticated organization's live apps.

### [Response](#favorite-app-response)

### [appId](#favorite-app-response-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#favorite-app-response-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#favorite-app-response-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

#### [id](#favorite-app-response-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [appId](#favorite-app-response-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [computeType](#favorite-app-response-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

#### [gpuType](#favorite-app-response-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

#### [fallbackGpuType](#favorite-app-response-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

#### [gpusPerWorker](#favorite-app-response-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

#### [minWorkers](#favorite-app-response-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

#### [maxWorkers](#favorite-app-response-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#favorite-app-response-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#favorite-app-response-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

#### [idleTtlSecs](#favorite-app-response-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

#### [scalingDelaySecs](#favorite-app-response-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

#### [requestTimeoutSecs](#favorite-app-response-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

#### [startupTimeoutSecs](#favorite-app-response-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

#### [createdAt](#favorite-app-response-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#favorite-app-response-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [health](#favorite-app-response-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

#### [state](#favorite-app-response-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

#### [reason](#favorite-app-response-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

#### [since](#favorite-app-response-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

#### [observedAt](#favorite-app-response-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

### [effectiveMaxWorkers](#favorite-app-response-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

### [runtime](#favorite-app-response-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

#### [activeWorkers](#favorite-app-response-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

#### [provisionedGpuCount](#favorite-app-response-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

#### [calculatedAt](#favorite-app-response-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

#### [requests24h](#favorite-app-response-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

#### [errorRate24h](#favorite-app-response-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

#### [averageRequestDuration24h](#favorite-app-response-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

#### [queueDepth](#favorite-app-response-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

### [secrets](#favorite-app-response-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

#### [id](#favorite-app-response-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [name](#favorite-app-response-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [type](#favorite-app-response-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

#### [metadata](#favorite-app-response-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

#### [createdAt](#favorite-app-response-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#favorite-app-response-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [envVarName](#favorite-app-response-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

### [environmentVariables](#favorite-app-response-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

#### [id](#favorite-app-response-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

#### [appId](#favorite-app-response-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [key](#favorite-app-response-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

#### [value](#favorite-app-response-environmentvariables-value)

- **Type**: `string`
- **Required**: true

#### [createdAt](#favorite-app-response-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#favorite-app-response-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [status](#favorite-app-response-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [isFavorite](#favorite-app-response-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

### [activeVersionId](#favorite-app-response-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

### [createdAt](#favorite-app-response-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [updatedAt](#favorite-app-response-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#favorite-app-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">403</code></td><td data-rw-cid-pz7tphoj="true">Authenticated but not permitted to access this resource</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">404</code></td><td data-rw-cid-pz7tphoj="true">Resource not found</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but semantically invalid (e.g. a missing or out-of-range field). A request that could not be parsed returns <code class="component-Code">400</code>. The <code class="component-Code">errors</code> array carries one entry per offending field.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable</td></tr></tbody></table>

## Remove a favorite

DELETEapi.serverless.runware.ai/v1/apps/{appId}/favorite

Removes the organization favorite pin from the app. Idempotent: unfavoriting an app that is not favorited succeeds and returns the app with `isFavorite: false`. Valid in any status including `deleting` and `deleted`. Unpinning is not an app lifecycle mutation. Missing apps return `404`.

### [Request](#unfavorite-app-request)

Path

### [appId](#unfavorite-app-path-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier, unique among the authenticated organization's live apps.

### [Response](#unfavorite-app-response)

### [appId](#unfavorite-app-response-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

### [appName](#unfavorite-app-response-appname)

- **Type**: `string`
- **Required**: true
- **Min**: `1`

Mutable display name. Must start and end with a non-whitespace character: it is what the console renders and what `sort=name` orders on, and it is not required to be unique. Interior spaces are allowed ("Sentiment Analysis"). Leading or trailing whitespace is rejected, because a padded name is indistinguishable from its trimmed form in the console and breaks a name-confirm delete.

### [configuration](#unfavorite-app-response-configuration)

- **Type**: `object (16 properties)`
- **Required**: true

Live worker configuration. Updated via `PATCH /apps/{appId}`.

#### [id](#unfavorite-app-response-configuration-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [appId](#unfavorite-app-response-configuration-appid)

- **Type**: `string`
- **Required**: true
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [computeType](#unfavorite-app-response-configuration-computetype)

- **Type**: `string`
- **Required**: true

Worker compute class. GPU is the only supported value. CPU workloads are not supported.

**Possible values**: `gpu`

#### [gpuType](#unfavorite-app-response-configuration-gputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Preferred GPU type. Absent (or null) only on historical apps created before a GPU type was required.

#### [fallbackGpuType](#unfavorite-app-response-configuration-fallbackgputype)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `64`

Declared substitute for `gpuType`. Placement uses it when the preferred pool has no recorded inventory, or no complete block is free. It is not used when placement contends, a scan reaches its limit, or credit does not cover `gpuType`. A kubernetes preferred pool that names a substitute is admitted by the coordinator webhook, which fails closed, so those pods are not created while the coordinator is down. A worker placed on it reports that type as `Worker.gpuType`. Workers of one app can be on either type. The substitute is charged at its own catalogue rate, including when that rate is higher than `gpuType`. A commitment covers only its own GPU type, so this placement does not borrow one bought for `gpuType`.

#### [gpusPerWorker](#unfavorite-app-response-configuration-gpusperworker)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Default**: `1`

GPUs granted to one worker pod. Create and update accept only the group sizes the cluster grants indivisibly, since a worker holds its GPUs as one such group. Historical apps may contain another value.

#### [minWorkers](#unfavorite-app-response-configuration-minworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `0`
- **Default**: `0`

Floor for scale-down. 0 = scale to zero.

#### [maxWorkers](#unfavorite-app-response-configuration-maxworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`

#### [minAvailableWorkers](#unfavorite-app-response-configuration-minavailableworkers)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`

Idle workers held above current demand, so a burst does not wait for a cold start. Null or 0 means no buffer. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`. A buffer below about a tenth of current demand is not added while demand holds steady. `availableWorkersPct` is not subject to that.

#### [availableWorkersPct](#unfavorite-app-response-configuration-availableworkerspct)

- **Type**: `integer (nullable)`
- **Format**: `int32`
- **Min**: `0`
- **Max**: `100`

Idle workers held above current demand, as a percentage of that demand, rounded up. Null or 0 means no buffer. When both buffers are set the larger of the two applies. The buffer applies only while the queue is non-empty: an idle app still scales to `minWorkers`.

#### [idleTtlSecs](#unfavorite-app-response-configuration-idlettlsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Seconds a worker can sit idle before the Scaler removes it.

#### [scalingDelaySecs](#unfavorite-app-response-configuration-scalingdelaysecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`

Cooldown between consecutive scaling decisions.

#### [requestTimeoutSecs](#unfavorite-app-response-configuration-requesttimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `900`

How long the sidecar waits on one forwarded container request. Container-only: a code app keeps the platform MLflow read ceiling, so this field is stored and returned but is not applied to those reads. Bounds the worker, not the synchronous HTTP wait: `invoke-sync` still answers 504 at the platform deadline so the caller can poll. A later `container.yaml` deploy overwrites this with that document's `timeouts.requestSeconds`.

#### [startupTimeoutSecs](#unfavorite-app-response-configuration-startuptimeoutsecs)

- **Type**: `integer`
- **Required**: true
- **Format**: `int32`
- **Min**: `1`
- **Max**: `2400`

How long readiness has before the pod is failed. Rendered as the startup probe budget and the sidecar's own startup gate. A later `container.yaml` deploy overwrites this with that document's `timeouts.startupSeconds`.

#### [createdAt](#unfavorite-app-response-configuration-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#unfavorite-app-response-configuration-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [health](#unfavorite-app-response-health)

- **Type**: `object (4 properties, read-only)`

Whether the app's workload can serve, and why. `reason` covers more than capacity: a workload that is missing or being torn down also reports `state: unavailable`. Only `capacity_below_floor`, `demand_unserved` and `placement_refused` are the platform being short of workers, and only those refuse an invocation with `capacity-unavailable`. The others are refused with the plain `service-unavailable`, because retrying does not bring a removed workload back. Populated on the app reads only: `GET /v1/apps/{appId}` and `GET /v1/apps`. Every other response that carries an app omits it, because the verdict is observed from the cluster rather than changed by the request: read the app again after a mutation. Within those two reads it is absent for any of four reasons: the platform has not observed the app yet, which is normal for one that has never deployed. The app is in a lifecycle state whose verdict is not published, such as `stopped`, `failed`, or one that is draining. The verdict could not be read on this request, or the stored verdict carries a value this version of the API does not recognise. The four are not distinguished, so **absence is never a claim that the app is healthy**. The first two are stable, the third clears by itself, and the last persists until the API is upgraded.

#### [state](#unfavorite-app-response-health-state)

- **Type**: `string`
- **Required**: true

Whether the app's workload can serve. `healthy` can serve. `degraded` can serve with less capacity than it asks for. `unavailable` has nothing able to serve. Read `AppHealthReason` for the cause. This is a report on the workload, not an admission rule. Only an `active` app is refused on it: an `initializing` app that already has a version to route to accepts invocations while it reports `unavailable`, which is the ordinary case during a first deploy, and a draining app is not gated on health at all. Do not read this field as whether the next invocation will be accepted.

**Possible values**: `healthy` `degraded` `unavailable`

#### [reason](#unfavorite-app-response-health-reason)

- **Type**: `string`
- **Required**: true

Why the app holds its current health state. `capacity_below_floor`, `demand_unserved` and `placement_refused` are the shapes of capacity exhaustion, and they differ in what the app asked for. `capacity_below_floor` means the app keeps a warm floor above zero and has fewer workers able to serve than that floor. `demand_unserved` means the app scales to zero, so it has no floor to be short of, and work is waiting on its queue with nothing running it. `placement_refused` is that same wait once the platform has refused to place a worker for lack of GPU capacity, and that refusal has stood for the scale-up grace. A refusal because credit does not cover the worker, or because the worker does not match its placement policy, is not this reason. A refused create is not stored, so the workers list stays empty. They behave differently during a cold start. An app waking from zero is given a grace period before it is called starved, so an ordinary wake-up is not reported as a fault. An app with a warm floor gets no such grace: it reports `capacity_below_floor` from the moment its workload is applied until its first worker is ready, so a normal first deploy reports it for the whole of its cold start. Use `since` to tell the two apart. A cold start clears within the app's startup time, and a real shortage does not. None of them names whose fault the shortfall is, and none is a statement about charging. A worker the platform never placed holds no GPU and costs nothing, but the same reasons also cover workers that were placed and cannot serve, and those hold a GPU. Some of those states are charged for and some are not: a container that crash-loops or one still loading is charged, while one still pulling its image is not. Read the app's workers to tell the cases apart. `placement_refused` is the case that list cannot show, because no pod exists. `start_failed` means no worker can serve and a worker has kept failing to start for about three attempts: two more startup budgets after its first failed start, without becoming ready. It names the cause rather than the shortfall: the app's own start keeps failing. A retry after a failed start is not charged, so the platform stops the app on this reason, records why on its events and notifies you. Fix the cause and resume or redeploy the app. A worker that keeps failing while other workers of the app serve is not this reason, and the app keeps running. `autoscaler_unhealthy` means the autoscaler cannot act on the workload, so the app will not grow with demand. `workload_present` accompanies a healthy app. `workload_terminating` and `workload_missing` are a workload being removed or already gone, which a stop or a delete explains.

**Possible values**: `workload_present` `autoscaler_unhealthy` `capacity_below_floor` `demand_unserved` `placement_refused` `start_failed` `workload_terminating` `workload_missing`

#### [since](#unfavorite-app-response-health-since)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the app entered this state. It moves only when `state` changes, so it answers how long the condition has held, the figure to quote when asking how long an app has been unable to serve.

#### [observedAt](#unfavorite-app-response-health-observedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

When the platform last looked. It is rewritten on every observation, so it reports the freshness of the verdict and not the age of the condition. A value far in the past means nothing has observed the app recently.

### [effectiveMaxWorkers](#unfavorite-app-response-effectivemaxworkers)

- **Type**: `integer (nullable, read-only)`
- **Format**: `int32`
- **Min**: `0`

The worker ceiling the last deploy actually applied, reduced where the organization's credit balance did not back the whole range. The autoscaler cannot grow past it.

It describes what was applied, not what is configured now, and the two can differ. It is taken from the `maxWorkers` of the version that was deployed, so deploying an older version applies that version's ceiling, and a later `PATCH` of `configuration.maxWorkers` does not change it until the next deploy. Read it beside `configuration.maxWorkers` rather than as a bound on it.

`null` means nothing has been applied yet. It is recalculated on every deploy. A credit top-up also recalculates a reduced ceiling and restores the funded range, with no redeploy.

### [runtime](#unfavorite-app-response-runtime)

- **Type**: `object (7 properties)`
- **Required**: true

Observed state for one app at `calculatedAt`. Desired worker scale remains in `configuration`. Worker and GPU counts are always present. Traffic, duration, and queue fields are omitted when their backing data is unavailable.

#### [activeWorkers](#unfavorite-app-response-runtime-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Non-terminal workers (`status` other than `stopped`) on this app's active version. Pending workers count because they appear in the active-version workers list before Kubernetes assigns a GPU. Outgoing-version workers are excluded because the default workers list omits them. A deleted app reports zero.

#### [provisionedGpuCount](#unfavorite-app-response-runtime-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`
- **Min**: `0`

Sum of `gpuCount` across those workers. A pending worker contributes zero until Kubernetes schedules it onto a node.

#### [calculatedAt](#unfavorite-app-response-runtime-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When this runtime snapshot was read from the database.

#### [requests24h](#unfavorite-app-response-runtime-requests24h)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Requests served by this app in the last 24 hours. Omitted until available.

#### [errorRate24h](#unfavorite-app-response-runtime-errorrate24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`
- **Max**: `1`

Error ratio (4xx + 5xx over requests) for this app in the last 24 hours, in 0–1. Omitted when metrics cannot be read or when the app had no requests in the window.

#### [averageRequestDuration24h](#unfavorite-app-response-runtime-averagerequestduration24h)

- **Type**: `number`
- **Format**: `double`
- **Min**: `0`

Mean inference request duration in seconds over the last 24 hours (all requests, the duration histogram has no status class). Omitted when metrics cannot be read, when the app had no requests, or when the duration series has no samples for the app in the window.

#### [queueDepth](#unfavorite-app-response-runtime-queuedepth)

- **Type**: `integer`
- **Format**: `int64`
- **Min**: `0`

Ready plus unacknowledged messages on this app's live inference queue. Omitted when the app is not live or the gauge cannot be read. Zero when the queue is live and empty.

### [secrets](#unfavorite-app-response-secrets)

- **Type**: `object[] (7 properties)`
- **Required**: true

Secrets attached to this app, including any env-var name override. Populated on single-app responses. List of apps returns an empty array to avoid an N+1. Use `/apps/{appId}/secrets` to page the set.

#### [id](#unfavorite-app-response-secrets-id)

- **Type**: `string (uuid)`
- **Required**: true
- **Format**: `UUID v4`

#### [name](#unfavorite-app-response-secrets-name)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

Organization-scoped secret name. The shape matches `EnvironmentVariableName` and the `secrets.name` / `deployment_secrets.env_var_name` column CHECKs, one rule for the contract and the schema, because attached secrets are intended to be injected as environment variables once ADR-019 in-pod unseal lands. Names the platform sets on the serving container (`RUNTIME`, `DISABLE_NGINX`, `MLFLOW_MODELS_WORKERS`, `UVICORN_HOST`) are rejected with `422`: when injection exists, the deployer appends customer env after its own and kubelet resolves duplicates last-wins, so an accepted collision would silently replace a platform value. Same guard as plain environment variables. Enforced by the server (not expressible as a pattern here).

#### [type](#unfavorite-app-response-secrets-type)

- **Type**: `string`
- **Required**: true

Kind of secret. Only the environment-variable variant (`generic`) is supported. Image-pull (`registry`) credentials are consumed by the kubelet before any container starts, so they cannot use the in-pod unseal path (ADR-019) and await their own decision.

**Possible values**: `generic`

#### [metadata](#unfavorite-app-response-secrets-metadata)

- **Type**: `object (nullable)`

Optional opaque metadata associated with the secret.

#### [createdAt](#unfavorite-app-response-secrets-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#unfavorite-app-response-secrets-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [envVarName](#unfavorite-app-response-secrets-envvarname)

- **Type**: `string (nullable)`
- **Min**: `1`
- **Max**: `128`

Resolved environment variable name when it differs from `name`. Omitted when the secret name is used. Same reserved-name rules as `SecretName` when set.

### [environmentVariables](#unfavorite-app-response-environmentvariables)

- **Type**: `object[] (6 properties)`
- **Required**: true

Plain-text environment variables for this app. Populated on single-app responses (get, update, stop, resume, delete, deploy, favorite). List of apps returns an empty array to avoid an N+1 per page row. Use the `/environment-variables` endpoints to page the set.

#### [id](#unfavorite-app-response-environmentvariables-id)

- **Type**: `string (uuid)`
- **Format**: `UUID v4`

#### [appId](#unfavorite-app-response-environmentvariables-appid)

- **Type**: `string`
- **Min**: `6`
- **Max**: `30`

Immutable app identifier. Unique among the authenticated organization's live apps: it cannot be changed after creation, and it becomes available again once the app it named reaches `deleted`.

#### [key](#unfavorite-app-response-environmentvariables-key)

- **Type**: `string`
- **Required**: true
- **Min**: `1`
- **Max**: `128`

POSIX-style environment variable name. Letters, digits, and underscore. Must start with a letter or underscore. Matched by the `deployment_configs` column CHECK so a valid-by-contract request cannot 500 at INSERT.

#### [value](#unfavorite-app-response-environmentvariables-value)

- **Type**: `string`
- **Required**: true

#### [createdAt](#unfavorite-app-response-environmentvariables-createdat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

#### [updatedAt](#unfavorite-app-response-environmentvariables-updatedat)

- **Type**: `string (date-time)`
- **Format**: `date-time`

### [status](#unfavorite-app-response-status)

- **Type**: `string`
- **Required**: true

Where the app is in its lifecycle. `initializing` is building or rolling out its first version. `active` is deployed and accepting invocations. `stopping` is draining its workers. `stopped` holds no workers and accepts none. `deleting` and `deleted` are removal. `failed` is a rollout the platform could not complete. A `failed` app accepts no invocations. Recover it with `POST /v1/apps/{appId}/deploy` when it has a `ready` version, or with a new `appSource` through `PATCH /v1/apps/{appId}`. Lifecycle is not health. `active` says the app is deployed and taking work, not that workers exist to run it: an app whose workers the platform cannot currently place stays `active`. Read `health` for that.

**Possible values**: `initializing` `active` `stopping` `stopped` `deleting` `deleted` `failed`

### [isFavorite](#unfavorite-app-response-isfavorite)

- **Type**: `boolean`
- **Required**: true

Whether the authenticated organization has favorited this app. Favorited apps sort ahead of non-favorited apps. Toggled via `PUT`/`DELETE` `/v1/apps/{appId}/favorite`.

### [activeVersionId](#unfavorite-app-response-activeversionid)

- **Type**: `string (uuid) (nullable)`
- **Format**: `UUID v4`

Current deployed version. Null until the first version is successfully deployed.

### [createdAt](#unfavorite-app-response-createdat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [updatedAt](#unfavorite-app-response-updatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

### [Errors](#unfavorite-app-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">403</code></td><td data-rw-cid-pz7tphoj="true">Authenticated but not permitted to access this resource</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">404</code></td><td data-rw-cid-pz7tphoj="true">Resource not found</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">422</code></td><td data-rw-cid-pz7tphoj="true">The request was well-formed but semantically invalid (e.g. a missing or out-of-range field). A request that could not be parsed returns <code class="component-Code">400</code>. The <code class="component-Code">errors</code> array carries one entry per offending field.</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable</td></tr></tbody></table>

## Summary across apps

GETapi.serverless.runware.ai/v1/app-summary

Aggregate dashboard metrics across all apps owned by the authenticated organization. App and worker tallies are always present. Request and error-rate totals come from the metrics store and are omitted when that hop cannot answer rather than reported as zero. Spend covers a rolling 24 hours and is omitted when the usage cannot be priced.

### [Response](#get-app-summary-response)

### [activeApps](#get-app-summary-response-activeapps)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`

Apps currently in the `active` status.

### [totalApps](#get-app-summary-response-totalapps)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`

All apps in the organization excluding soft-deleted ones.

### [activeWorkers](#get-app-summary-response-activeworkers)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`

Non-terminal workers (`status` other than `stopped`) across every app and version in the organization.

### [provisionedGpuCount](#get-app-summary-response-provisionedgpucount)

- **Type**: `integer`
- **Required**: true
- **Format**: `int64`

Sum of `gpuCount` across those same non-terminal workers.

### [requests24h](#get-app-summary-response-requests24h)

- **Type**: `integer`
- **Format**: `int64`

Requests served in the last 24 hours across every app. Omitted when metrics cannot be read. Zero when the organization had no traffic.

### [errorRate24h](#get-app-summary-response-errorrate24h)

- **Type**: `number`
- **Format**: `double`

Error ratio (4xx + 5xx over requests) for the last 24 hours, in 0–1. Omitted when metrics cannot be read or when `requests24h` is zero.

### [spendToday](#get-app-summary-response-spendtoday)

- **Type**: `object (2 properties)`

Provisional pay-as-you-go accrual over the last 24 hours, across every app. Excludes finalization rounding and ledger adjustments. A rolling window rather than a calendar day, so the figure does not reset at a boundary the customer did not choose. What has accrued, not a projection: an app that has just started shows what it has run so far. Time a capacity commitment covered is excluded, because it collects nothing. Omitted when the usage cannot be priced, which a zero would misreport as having run for free.

#### [amount](#get-app-summary-response-spendtoday-amount)

- **Type**: `string`
- **Required**: true

Amount in major units as an exact decimal string.

#### [currency](#get-app-summary-response-spendtoday-currency)

- **Type**: `string`
- **Required**: true

ISO 4217 alphabetic code. The platform bills in USD only.

**Possible values**: `USD`

### [calculatedAt](#get-app-summary-response-calculatedat)

- **Type**: `string (date-time)`
- **Required**: true
- **Format**: `date-time`

When these metrics were computed.

### [Errors](#get-app-summary-errors)

<table><tbody><tr><td data-rw-cid-pz7tphoj="true">Status</td><td data-rw-cid-pz7tphoj="true">When</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">401</code></td><td data-rw-cid-pz7tphoj="true">Missing or invalid credentials</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">500</code></td><td data-rw-cid-pz7tphoj="true">Unexpected server error</td></tr><tr><td data-rw-cid-pz7tphoj="true"><code data-rw-cid-pz7tphoj="">503</code></td><td data-rw-cid-pz7tphoj="true">A required service is temporarily unavailable</td></tr></tbody></table>