Docs
Open the console →
API reference

API reference: Terraform

Snowtrol Terraform API: blueprints, workspaces (folders), plan/approve/apply/drift/discard, workspace membership and edits, multi-blueprint folders, import/adopt, state versions and the http state backend.

The Terraform execution engine — the platform's single mutation path (plan → policy/cost gates → approve → apply the saved plan → inventory). Reads are open to any member; every mutation requires mutate_infra (Viewers get 403); cross-org ids return 404. Runs serialize per workspace. When the deployment runs the async runner (CC_TF_ASYNC=1), plan and apply return { state: "queued" } and the run progresses queued → running → planned/succeeded/failed — poll the workspace detail; with async off they run inline and return the finished result.

GET/api/terraform/blueprintsSession or Bearer

The deployable blueprint catalog: built-in registry blueprints + enabled platform-managed + your org's own custom blueprints (tier custom). Supports q= and provider= filters.

Example response
json
{ "blueprints": [ { "id": "aws/s3-bucket", "title": "S3 bucket", "provider": "AWS",
  "tfType": "aws_s3_bucket", "resourceType": "storage", "fields": [ { "key": "name", "type": "string" } ] } ] }
Errors
StatusWhen
401Not signed in.
POST/api/terraform/workspacesmutate_infra

Create a Terraform workspace (folder) from a blueprint against a healthy cloud account — or a BLANK workspace with no blueprint. The criticality sets the approval policy: 'production' (default) requires a second approver; 'standard' lets the plan author self-approve. Optional varBindings bind variables to other workspaces' outputs; gitConnectionId picks which infra repo the workspace hydrates into; autoPipeline:false opts out of the auto-created infrastructure pipeline.

Parameters
NameInRequiredDescription
blueprintIdbodynoA blueprint id from GET /api/terraform/blueprints (omit for a blank workspace).
cloudAccountIdbodyyesA healthy cloud account in your org, matching the blueprint's provider.
namebodyyesLowercase letters, digits, hyphens (2–41 chars).
regionbodynoDeploy region (defaults from the blueprint).
criticalitybodyno'production' (default — approver ≠ author) or 'standard' (self-approve allowed).
varBindingsbodyno{ varKey: { fromWorkspaceId, outputKey } } output bindings.
gitConnectionIdbodynoAn infra git connection id (defaults to the org's first writable infra repo).
Example request
json
{ "blueprintId": "aws/s3-bucket", "cloudAccountId": 3,
  "name": "web-assets", "region": "us-east-1", "criticality": "standard" }
Example response
json
{ "workspace": { "id": 7, "provider": "AWS", "region": "us-east-1",
  "blueprintId": "aws/s3-bucket", "name": "web-assets", "status": "active",
  "criticality": "standard", "currentStateVersion": 0 } }
Errors
StatusWhen
400Unknown blueprint, bad name, provider mismatch, unhealthy account, invalid criticality/bindings, or a non-infra gitConnectionId.
403Role lacks mutate_infra (Viewer).

GET /api/terraform/workspaces lists workspaces; GET /api/terraform/workspaces/{id} returns the detail — runs (with persisted {seq,level,line} logs), members, applied outputs, dependency links, and the blueprint instances the folder combines. PATCH /api/terraform/workspaces/{id} replaces { varBindings }. DELETE /api/terraform/workspaces/{id} with { confirm: "<exact name>" } destroys the managed resources (a workspace whose outputs feed live dependents refuses with 409); DELETE …?purge=1 removes a destroyed workspace entirely and prunes its hydrated directory from the connected repo.

POST/api/terraform/workspaces/{id}/planmutate_infra

Compose the module from the blueprint(s)/members and the submitted variables, run the plan, evaluate policy (no-public-bucket / mandatory-tags / region-allowlist) and estimate the cost delta. Bound variables resolve server-side from the source workspace's applied outputs. The plan is saved on a run; new resources appear in Inventory as drafts (◌ planned). Async deployments return { state: "queued" }.

Example response
json
{ "runId": 12, "state": "planned", "diff": { "add": 1, "change": 0, "destroy": 0 },
  "policy": { "pass": true, "checks": [ … ] }, "costDelta": 8.5, "hash": "…" }
Errors
StatusWhen
400Variable validation failed, or a bound output is missing/destroyed (the error names the workspace and available outputs).
403Role lacks mutate_infra.
409A run is already open on this workspace (discard it or finish it).
POST/api/terraform/workspaces/{id}/approvemutate_infra

Approve a planned run. The policy must pass. On a 'production' workspace the approver must differ from the plan's author (separation of duties, 403 on self-approve); on a 'standard' workspace the plan author may approve their own run.

Parameters
NameInRequiredDescription
idpathyesWorkspace id in your org.
runIdbodyyesThe planned run to approve.
Example request
json
{ "runId": 12 }
Example response
json
{ "runId": 12, "state": "approving", "approverId": 5 }
Errors
StatusWhen
403Role lacks mutate_infra; policy failed; or (production workspace) the approver is the plan's author.
404No such workspace/run in your org (cross-org / unknown).
POST/api/terraform/workspaces/{id}/applymutate_infra

Apply the SAVED, approved plan (integrity-hashed — never re-planned). By default the apply is recorded-only (modeled in inventory + state). It is REAL — OpenTofu runs against your live cloud with freshly-minted short-lived credentials — only when the platform runs CC_TF_RUNNER=real AND the target account has provisioning enabled; otherwise a real-runner apply is refused 409 { provisioningDisabled: true }. A failed apply marks the run failed with its logs, leaves state and inventory untouched, and keeps the workspace usable.

Example response
json
{ "runId": 12, "state": "succeeded", "created": 1, "stateVersion": 1, "outputs": { "bucket_name": "web-assets" } }
Errors
StatusWhen
403Run not approved / role lacks mutate_infra.
409Real runner active but the account has not opted into provisioning, or the blast-radius guard tripped (blastRadius: true — confirm to proceed).

Also on a workspace: POST …/drift (refresh run comparing applied state to live inventory), POST …/discard (discard the open run and remove its draft rows), GET/POST …/state (list encrypted state versions / roll back to { version }), GET …/hydrated (the composed module — object + rendered native HCL main.tf).

Folders: membership, edits and combined blueprints

POST/api/terraform/workspaces/{id}/resourcesmutate_infra

Assign / MOVE a synced resource INTO this workspace (folder). A workspace is a folder of resources; this reassigns membership and recompiles the target AND the resource's previous folder from all their members, returning both recompiled diffs + any address conflicts.

Parameters
NameInRequiredDescription
idpathyesTarget workspace (folder) id in your org.
nativeIdbodyyesThe resource's native id (from GET /api/cloud/resources).
Example request
json
{ "nativeId": "i-0abc123def4567890" }
Example response
json
{ "nativeId": "i-0abc123def4567890", "previousWorkspaceId": null, "targetWorkspaceId": 7,
  "to": { "workspaceId": 7, "workspaceName": "web-tier", "memberCount": 1,
    "diff": { "add": 1, "change": 0, "destroy": 0 }, "conflicts": [], "committed": true,
    "stateVersion": 1, "repoWritten": true }, "from": null }
Errors
StatusWhen
400Missing nativeId / body isn't JSON / the workspace targets a different provider.
403Role lacks mutate_infra (Viewer).
404No such workspace or resource in your org (cross-org / unknown).
409The target workspace has been destroyed.
PATCH/api/terraform/workspaces/{id}/resources/{nativeId}mutate_infra

Edit a MANAGED member's settings (Edit-from-Inventory). The edits are validated against a per-type allowlist (e.g. compute instanceType, storage storageClass, network/subnet cidr, routetable routes, tags), staged on the resource, and the folder recompiles into a PLANNED run (diff ~1 to change + policy + Δ$/mo) that the normal approve/apply routes finish.

Parameters
NameInRequiredDescription
editsbodyyesObject of allowlisted attribute edits for the resource's type.
Example request
json
{ "edits": { "instanceType": "t3.large", "tags": { "env": "prod" } } }
Errors
StatusWhen
400A non-allowlisted key, bad shape, or a no-op edit.
404Not a member of this folder in your org.
409Destroyed workspace or a run already open.
POST/api/terraform/workspaces/{id}/blueprintsmutate_infra

Add another blueprint to this folder ({ blueprintId, vars }). One workspace can deploy MANY blueprints — the folder composes ALL its blueprint instances into one module, so the next plan shows the addition alongside what it already manages. GET lists the folder's blueprint instances.

Example request
json
{ "blueprintId": "aws/ec2-instance", "vars": { "name": "worker-1" } }
Errors
StatusWhen
400Unknown blueprint, provider mismatch, or bad vars.
409Destroyed workspace or a run already open.
DELETE/api/terraform/workspaces/{id}/resources/{nativeId}mutate_infra

Remove a resource from this folder (unmanage it) and recompile the folder — the removed member shows as a delete in the diff. Native ids containing slashes (GCP/S3) can be passed via a ?nativeId= query, which wins over the path segment.

Example response
json
{ "nativeId": "i-0abc…", "previousWorkspaceId": 7, "targetWorkspaceId": null,
  "from": { "workspaceId": 7, "diff": { "add": 0, "change": 0, "destroy": 1 }, "memberCount": 0 }, "to": null }
Errors
StatusWhen
403Role lacks mutate_infra (Viewer).
404The resource is not a member of this folder in your org (cross-org / unknown).
POST/api/terraform/workspaces/{id}/compilemutate_infra

Recompile the folder module from its member resources and return the diff + any conflicts, WITHOUT changing membership — the drift/recompile check. { commit: true } regenerates + commits (new state version + best-effort repo write) when there are no conflicts.

Example request
json
{ "commit": true }
Example response
json
{ "workspaceId": 7, "workspaceName": "web-tier", "memberCount": 2,
  "diff": { "add": 0, "change": 0, "destroy": 0 }, "conflicts": [], "committed": false, "stateVersion": 3 }
Errors
StatusWhen
403Role lacks mutate_infra (Viewer).
404No such workspace in your org (cross-org / unknown).
POST/api/terraform/importmutate_infra

Import (adopt) any discovered resource into Terraform — including IAM (roles, policies, users, groups) and, via a generic fallback, any other service: generates real HCL + a Terraform import block (with the correct AWS import id per type — IAM roles/users/groups by name, a managed policy by ARN), records the adopt, marks the resource managed, and best-effort commits the HCL to your connected repo. See the Import existing resources guide.

Parameters
NameInRequiredDescription
nativeIdbodyyesThe discovered resource's native id (from GET /api/cloud/resources, managed:false).
Example request
json
{ "nativeId": "vpc-0aa1b2c3d4e5f0001" }
Example response
json
{ "managed": true, "nativeId": "vpc-0aa1b2c3d4e5f0001", "resourceType": "network",
  "tfType": "aws_vpc", "address": "aws_vpc.prod-use1", "mapped": true,
  "workspaceId": 7, "workspaceName": "imported-aws-us-east-1", "runId": 12,
  "stateVersion": 1, "repoPath": "terraform/imported/aws_vpc-prod-use1.tf",
  "repoWritten": true, "commitRef": "63c0111…" }
Errors
StatusWhen
400Missing nativeId / body isn't JSON.
403Role lacks mutate_infra (Viewer).
404No such resource in your org (cross-org / unknown id).
409The resource is already under Terraform management.

The http state backend (opt-in)

With CC_TF_STATE=turso_http, GET/POST /api/terraform/state/{id} (fetch / persist current state) and POST/DELETE /api/terraform/state/{id}/lock (advisory lock / unlock) serve Terraform's http backend protocol — Store-backed, encrypted per org, org-scoped (cross-org 404). A concurrent lock on a held workspace answers `423 Locked` with the holder's info; every POST appends a state version, preserving history and rollback. The real runner's child tofu authenticates with an ephemeral, workspace-scoped HTTP Basic state token minted per apply (username cc-state); humans use the normal session/Bearer path. With the default CC_TF_STATE=internal these routes are dark (404).