# Create template deployment

`POST https://api.instacloud.com/projects/{projectId}/template-deployments`

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

Deploy a template onto a branch (async — poll GET /template-deployments/:id). Omit branch/branchId for a fresh branch named after the template. Deploying the same template into a branch that already runs it builds an INDEPENDENT copy — its services are named <name>-2, <name>-3 … and the existing copy is untouched — so the names in the response need not match the manifest. Gated — service.add + secrets.write + deploy, plus service.upgrade when the manifest declares a volume and service.setAccess when it declares a public bucket. Idempotent per (deploymentId, manifest): duplicate/mid-run re-invokes answer 202 with the current row.

## Parameters

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

## Request body (application/json)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `templateCode` | `string` | no | a published registry code (see GET /templates); mutually implied with manifest — one of the two is required |
| `code` | `string` | no | alias of templateCode |
| `templateVersion` | `string` | no | optional pin; 404s when the registry serves a different version |
| `manifest` | `any` | no | inline insta.template.yaml for a local-manifest deploy — the YAML text, or the parsed JSON document |
| `branchId` | `string` | no |  |
| `branch` | `string` | no | branch NAME (alias of branchId). Omit both for a fresh branch named after the template code |
| `variables` | `object` | no | deploy-time variable values (required/optional env vars declared by the manifest) |
| `deploymentId` | `string` | no | idempotent retry: resume THIS deployment (same services, no duplicates) instead of starting a new one |
| `region` | `string` | no | InstaCloud region slug for EVERY service this deployment creates (see GET /regions). Default: the platform default region. Part of the deployment identity: a retry may omit it or repeat it, a different value is refused with 409. |

## Response 202

accepted (deploymentId + deployment to poll) — or approval_required from the governance gate

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `deploymentId` | `string` | no | present when the deployment was accepted |
| `deployment` | `object` | no | One server-side template deployment run and its progress. |
| `deployment.id` | `string` | no |  |
| `deployment.status` | `"running" \| "succeeded" \| "failed" \| "partial"` | no | partial = some services came up healthy, others failed — created resources are kept either way |
| `deployment.step` | `"create_services" \| "write_variables" \| "deploy" \| "health_check"` | no | the pipeline step the run is currently on (or stopped at) |
| `deployment.templateCode` | `string` | no |  |
| `deployment.templateVersion` | `string` | no |  |
| `deployment.projectId` | `string` | no |  |
| `deployment.branchId` | `string` | no |  |
| `deployment.region` | `string` | no | InstaCloud region slug every service of this deployment was created in. Deployments recorded before regions were recorded read as the platform default. |
| `deployment.services` | `object[]` | no |  |
| `deployment.services.name` | `string` | no | the service's actual name on the branch, which is the manifest's name only for the first copy — a second deployment of the same template into the same branch names its services <name>-2 and leaves the first copy alone |
| `deployment.services.serviceId` | `string` | no |  |
| `deployment.services.url` | `string` | no |  |
| `deployment.services.state` | `"pending" \| "created" \| "deployed" \| "healthy" \| "failed"` | no |  |
| `deployment.services.skipped` | `boolean` | no | an upgrade stepped over this service because the user had deleted it — the run never touched it, serviceId points at the deleted row, and state stays pending however the deployment ends. Not "still in progress" |
| `deployment.error` | `string` | no |  |
| `deployment.logsTail` | `string` | no | container log tail captured when a service never became healthy |
| `deployment.rollbackTo` | `object` | no | the version one step back, present only when this deployment was produced by an upgrade: what POST .../rollback would return the instance to. Absent on a first deploy, once the predecessor is gone, and on a deployment a ROLLBACK produced — that row's step back is the newer version it just left, which is offered as the update, not as a way back. Present is not a promise it will succeed — a version whose manifest is no longer recorded, or was republished with different content, still refuses at click time |
| `deployment.rollbackTo.version` | `string` | yes |  |
| `deployment.createdAt` | `string` | no |  |
| `status` | `"approval_required"` | no |  |
| `approvalId` | `string` | no |  |
| `action` | `string` | no | the gated action, or a comma-joined compound set — prefer `actions` |
| `actions` | `string[]` | no | every capability this approval covers |
| `message` | `string` | no |  |
| `url` | `string` | no | the console page where a project admin reviews this request |
| `nextActions` | `object[]` | no |  |
| `nextActions.op` | `string` | yes | Neutral logical action id, e.g. "service.add" — NOT an operationId or a CLI/MCP tool name; each client maps it to its own surface. |
| `nextActions.reason` | `string` | yes | Natural-language, human/LLM-facing "why do this now". |
| `nextActions.args` | `object` | no | Suggested, flat named arguments; "<placeholder>"s where a value is unknown. |
| `nextActions.gated` | `boolean` | no | True if the action passes a governance gate. |

## Response 400

Default Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | `string` | yes |  |
| `missing` | `object[]` | no | error='missing_variables': the required variables that resolved to no value |
| `missing.name` | `string` | yes |  |
| `missing.key` | `string` | yes |  |
| `missing.description` | `string` | no |  |

## Response 403

Default Response

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

## Response 404

Default Response

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

## Response 409

the run is still in progress; the branch already holds the maximum number of copies of this template; or a service name is too long to suffix for another copy. Also code template_version_changed: the template was republished since it was read, fetch it again.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | `string` | yes |  |
| `code` | `string` | no | stable machine-readable reason, e.g. private_lane_disabled, private_lane_unavailable_in_region, public_access_closed, credential_change_in_progress |

## Response 429

Default Response

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