# Upgrade service

`POST https://api.instacloud.com/projects/{projectId}/services/{serviceId}/upgrade`

**Token scope:** `account` · `org` · `project` — read-only tokens refused (not a GET).

Change a compute/postgres service's spec (paid plans only; up-only).

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `projectId` | path | `string` | yes |  |
| `serviceId` | path | `string` | yes |  |

## Request body (application/json)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `spec` | `string` | yes | compute: a named rung 1vcpu-256mb..8vcpu-8gb (compute services only); up-only, and never above the org's plan ceiling |

## Response 200

Default Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `service` | `object` | no | A project-level service (postgres \| storage \| compute \| redis \| mysql \| mongodb). |
| `service.id` | `string` | no |  |
| `service.project_id` | `string` | no |  |
| `service.type` | `"postgres" \| "storage" \| "compute" \| "redis" \| "mysql" \| "mongodb"` | no |  |
| `service.name` | `string` | no |  |
| `service.spec` | `object` | no | compute: {cpu_kind,cpus,memory_mb}; postgres: sized by its instance ceiling (cpuMilli/memoryMib) |
| `service.machine_count` | `integer` | no | compute only |
| `service.desired_state` | `"running" \| "stopped" \| "suspended"` | no | compute only — developer-set lifecycle intent |
| `service.always_on` | `boolean` | no | compute and managed databases (redis, mysql, mongodb) — true: machines never scale to zero; false: idle machines suspend and wake on request. A new compute service is born false (scale-to-zero, except a worker); a new managed database is born from INSTA_DB_ALWAYS_ON_DEFAULT (true on InstaCloud). Postgres is not recorded here: its idle mode lives on the database instance (GET /projects/{projectId}/database/instance, scaleToZero). Billing is actual usage either way. |
| `service.websocket` | `boolean \| null` | no | compute only — the app speaks WebSocket, so its machines carry connections-based concurrency and a 512 MB guest floor. Re-asserted by any redeploy given no flag, including restart. null: never recorded. |
| `service.endpoint` | `"http" \| "tcp" \| "udp" \| "tcp+udp" \| null` | no | compute only — how the service is reached. http: Host-routed HTTPS at `domain`. tcp / udp / tcp+udp: raw TCP, UDP, or both at `endpoint_host`:`port` on the service’s own public IPv6 address (IPv6 clients only; no TLS from the platform, no custom domain, no WebSocket flag). Chosen at create, and changed by a deploy that passes `endpoint`. null: never chosen, which is http. |
| `service.endpoint_host` | `string \| null` | no | compute, endpoint tcp/udp/tcp+udp only — the host to reach on `port` (an IPv6-only name), set once a deploy has confirmed it; null otherwise |
| `service.region` | `string \| null` | no |  |
| `service.domain` | `string \| null` | no | default access domain (postgres/compute) |
| `service.public` | `boolean` | no | storage — bucket(s) served with anonymous public-read; managed databases — the database port is exposed on a public address (on insta-compute, false = reachable only over the private network, *_PRIVATE_URL); postgres — the public endpoint accepts connections (true by default; false = not reachable from the internet, private network lane only); compute — the service answers the internet at its URL / endpoint_host (true by default; false = the URL answers 403 and never wakes it, and only services in the same branch reach it, at private_host). Change it with PUT .../services/{serviceId}/access |
| `service.private_host` | `string \| null` | no | compute and managed databases on insta-compute — the name other services in the SAME project branch reach this one at, on `port` over TCP: `<name>.insta.internal` for compute, `<name>-<type>.insta.internal` for a managed database (redis/mysql/mongodb). Other branches and projects cannot reach it. Resolves through the search domain every service is started with; tools that skip the search list (nslookup, dig) need private_fqdn. null: the service has no private address (a worker, a name ending in '-', a service not yet back-filled, or a region without the private network) |
| `service.private_fqdn` | `string \| null` | no | compute and managed databases on insta-compute — the fully-qualified form of private_host, `<private name>.<network>.insta.internal`, for clients that do not use the search domain. null whenever private_host is |
| `service.image` | `string \| null` | no | compute only — container image currently run |
| `service.port` | `integer \| null` | no | compute/managed database — compute image port, or the private database TCP port |
| `service.start_command` | `string \| null` | no | Runtime startup override, run via sh -c; on insta-compute it runs in place of the image's ENTRYPOINT and CMD: a wrapper entrypoint (e.g. docker-entrypoint.sh) does not run while it is set. null restores the image's own ENTRYPOINT and CMD. Stored as readable service settings. |
| `service.volume_applied_mount_path` | `string \| null` | no | Last successfully confirmed Compute mount path; null means unknown, not proof of a changed path. |
| `service.volume_mount_path` | `string \| null` | no | Compute volume mount path; null or omitted means /data for legacy volumes. Changes apply on deployment. |
| `service.volume_gib` | `integer \| null` | no | compute/managed database — size (whole Gi) of the persistent data volume; null = no volume. Compute can attach/grow via PUT .../volume; managed databases are platform-managed in v1 |
| `service.pg_version` | `integer \| null` | no | postgres only — the Postgres MAJOR version the service runs (e.g. 16), known before the instance is ever connected to or woken. Pick client tooling (pg_dump/pg_restore/psql) of the same major. null: never recorded (a legacy row). The exact server_version of a RUNNING instance is on GET .../database/metrics |
| `service.template_deployment_id` | `string \| null` | no | the template deployment that created this service (survives branch clones); null for services created directly |
| `service.template_modified` | `boolean` | no | true once the service was reconfigured away from its template-deployed spec |
| `service.template_code` | `string \| null` | no | the template this service was deployed from; null for services created directly, and for one whose deployment record has been pruned |
| `service.template_logo_url` | `string \| null` | no | the template's mark; null when the template is not in the registry (or carries no logo). Official templates: a commit-pinned jsDelivr URL (TEMPLATE_LOGO_HOST). Community templates: any https:// link the author set. Joined live rather than stored, because a republish moves it |
| `service.template_name` | `string \| null` | no | the template's name as the registry spells it, for a surface people read; null when the template is not in the registry, or its manifest omits a name. Read from the manifest the registry serves NOW, beside the logo, because a republish can rename it |
| `service.template_version` | `string \| null` | no | the template version this instance was deployed from; null for a service created directly. Read from the deployment record, so it does not move when the registry is republished |
| `service.template_latest_version` | `string \| null` | no | the version the registry serves for this template now; null when the registry does not serve it — absent, unpublished or taken down. The row still reports template_code and template_name in those cases, because attribution of a resource its owner deployed is not catalog visibility. For display beside template_version, never for deciding whether an update exists |
| `service.template_update_available` | `boolean` | no | true when upgrading this service's template deployment to the version the registry serves now would actually change something about the services it runs: a setting the new manifest would write, an environment variable it would write differently, a service it would add, or a variable it now requires that this instance holds no value for. A republish that only moved presentation is not an update, because applying it would change nothing, and neither is a new version string on identical content, which is what every community republish produces. False whenever the answer cannot be determined: a template absent from the registry, unpublished, taken down, a manifest this version of the platform cannot parse, or a registry read that failed. False too while the deployment's own run has not succeeded, because what an unfinished deploy needs is a retry, not an upgrade |
| `service.source` | `object` | no | compute only — where the running image comes from: an image somebody deployed (type=image) or a connected repository (type=github). Full resource: GET /projects/{id}/services/{serviceId}/source |
| `service.source.type` | `"image" \| "github"` | yes |  |
| `service.source.owner` | `string` | no |  |
| `service.source.repo` | `string` | no |  |
| `service.source.branch` | `string` | no | the repo branch this service tracks |
| `service.source.auto_deploy` | `boolean` | no | a push to `branch` rebuilds and redeploys the service |
| `service.status` | `"creating" \| "active" \| "error" \| "deleting" \| "deleted"` | no |  |
| `service.created_at` | `string` | no |  |

## Response 202

Default Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | `string` | yes |  |
| `approvalId` | `string` | yes |  |
| `action` | `string` | yes |  |
| `actions` | `string[]` | yes |  |
| `message` | `string` | yes |  |
| `url` | `string` | yes |  |

## Response 400

Default Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | `string` | yes |  |

## Response 403

Default Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | `string` | yes |  |

## Response 404

Default Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | `string` | yes |  |
