The StackShip REST API lets you read and manage projects, workspaces, runs, variables, policies, credentials, and the private module registry from your own code.
Base URL and OpenAPI document
https://api.stackship.run/v1The OpenAPI 3.1 document lists every operation and can generate a client:
https://api.stackship.run/v1/openapi.jsonRequests and responses are JSON, except log, state, and artifact downloads, which stream.
Authentication
Send an OAuth access token in the Authorization header:
curl https://api.stackship.run/v1/workspaces \
-H "Authorization: Bearer $STACKSHIP_TOKEN"There are no personal API keys. Use the same OAuth access token that MCP clients receive:
- Discover the authorization server from
https://mcp.stackship.run/.well-known/oauth-protected-resource. - Register a public client, or reuse one you already registered.
- Run the OAuth 2.1 authorization code flow with PKCE, using
https://mcp.stackship.run/mcpas theresource. - Sign in, choose an organization, and approve the consent screen.
| Scope | Allows |
|---|---|
mcp:tools |
Read operations. Always required. |
mcp:write |
Changes such as creating runs, approving plans, and editing variables. Request it explicitly. |
offline_access |
A refresh token, so your integration can renew access for up to 30 days. |
Access tokens expire after one hour, work only in the organization you chose, and never exceed your role.
These tokens can call only the operations the MCP server exposes. Deletes,
token creation, credential changes, apply recovery, and sensitive downloads
return 403 MCP_OPERATION_NOT_SUPPORTED; do those in the dashboard.
Responses
Successful responses wrap the result in data:
{
"data": {
"id": "run_8f2k3m",
"object": "run",
"status": "awaiting_approval"
},
"meta": {
"request_id": "req_4b7c1d"
}
}Every response, including errors, carries a StackShip-generated request ID in
the X-Request-Id header, and in meta.request_id for JSON. Include it when
you contact support.
A request ID you send is never used as the request ID. To correlate with your
own logs, send an X-Request-Id header of up to 128 letters, digits, and .,
_, :, =, or - characters; it’s echoed in the X-Client-Request-Id
response header. Other values are ignored.
Pagination
The organization run list, a workspace’s run list
(GET /v1/workspaces/{workspace_id}/runs), workspace state versions, audit
events, the organization’s credential profiles (GET /v1/credential-profiles,
each with its workspace_id), and the organization’s policy versions
(GET /v1/policy-versions, each with its policy_set_id) are paginated with
cursors.
| Parameter | Description |
|---|---|
limit |
Items per page, from 1 to 100. Defaults to 50. |
cursor |
The next_cursor value from the previous page. |
curl "https://api.stackship.run/v1/runs?limit=50" \
-H "Authorization: Bearer $STACKSHIP_TOKEN"{
"data": [],
"meta": {
"request_id": "req_4b7c1d",
"page": { "next_cursor": "v1.eyJ0Ijo..." }
}
}Keep requesting with cursor until next_cursor is null. Cursors are
opaque and only valid with the endpoint that returned them.
Filter the organization run list
GET /v1/runs accepts filters, applied before pagination:
| Parameter | Description |
|---|---|
status |
One or more run statuses, such as awaiting_approval or applying. Repeat the parameter (status=planning&status=queued) or separate values with commas (status=planning,queued). A run matches if it has any of them. |
kind |
One or more run kinds: plan_and_apply, plan_only, speculative, or drift_check. Repeat the parameter or separate values with commas, like status. |
workspace_id |
Only runs in this workspace (ws_...). |
curl "https://api.stackship.run/v1/runs?status=awaiting_approval&workspace_id=ws_9x2p" \
-H "Authorization: Bearer $STACKSHIP_TOKEN"An unknown status returns 400 RUN_LIST_STATUS_INVALID, an unknown kind
returns 400 RUN_LIST_KIND_INVALID, and a value that
isn’t a workspace ID returns 400 RUN_LIST_WORKSPACE_INVALID. Keep the same
filters on every page you request with cursor. See
Run lifecycle for the statuses.
Errors
Errors return an HTTP status and a stable code:
{
"error": {
"code": "WORKSPACE_NAME_TAKEN",
"message": "A workspace with this name already exists."
},
"meta": { "request_id": "req_4b7c1d" }
}| Status | Common codes | Meaning |
|---|---|---|
400 |
VALIDATION_FAILED, IDEMPOTENCY_KEY_REQUIRED, RUN_LIST_STATUS_INVALID, RUN_LIST_KIND_INVALID |
The request is malformed or missing a required field or header. |
401 |
UNAUTHENTICATED |
The token is missing, invalid, or expired. Get a new token. |
403 |
FORBIDDEN, MCP_SCOPE_INSUFFICIENT, MCP_OPERATION_NOT_SUPPORTED |
Your role, the token’s scopes, or the token type doesn’t allow this operation. |
404 |
NOT_FOUND |
The resource doesn’t exist or isn’t in your organization. |
409 |
IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_IN_PROGRESS, APPROVAL_STALE, STATE_INPUT_OUTDATED |
The request conflicts with an earlier request or with the resource’s current state. |
413 |
PAYLOAD_TOO_LARGE |
The request body is too large. |
429 |
CREDENTIAL_VERIFICATION_RATE_LIMITED |
Too many attempts in a short time. Wait and retry. |
409 STATE_INPUT_OUTDATED means the workspace’s state changed while the run
was being queued. Nothing was started; create the run again.
Unknown paths return 404 in the same format. Branch on code, not
message; messages can change.
Safe retries with Idempotency-Key
Every POST, PUT, PATCH, and DELETE requires an Idempotency-Key
header. Use a new unique value, such as a UUID, for each intended change.
curl -X POST https://api.stackship.run/v1/workspaces/ws_9x2p/variables \
-H "Authorization: Bearer $STACKSHIP_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c0b9e-2d4a-4b7e-9c3f-1a2b3c4d5e6f" \
-d '{
"category": "terraform",
"key": "instance_type",
"value": "t3.small",
"description": "Default instance size",
"hcl": false,
"sensitive": false
}'If a request times out or gets no response, resend it with the same key and body. You get the original result; the change isn’t applied twice.
- A key is 1–255 printable characters and is remembered for 24 hours.
- Reusing a key with a different body returns
409 IDEMPOTENCY_KEY_REUSED. - Retrying while the first request is still running returns
409 IDEMPOTENCY_IN_PROGRESS. Wait briefly and retry. - Retrying a workspace token creation with the same key more than 24 hours
later returns
409 IDEMPOTENCY_KEY_EXPIRED. The token was already issued and isn’t shown again; use a new key to create another.
Examples
Check a run
curl https://api.stackship.run/v1/runs/run_8f2k3m \
-H "Authorization: Bearer $STACKSHIP_TOKEN"See Run lifecycle for each status value.
Approve a plan
Approvals are tied to the exact plan you reviewed.
-
Read the review and save its
data.approval_bindingobject toapproval_binding.json:curl https://api.stackship.run/v1/runs/run_8f2k3m/review \ -H "Authorization: Bearer $STACKSHIP_TOKEN" -
Send that object unchanged as the request body:
curl -X POST https://api.stackship.run/v1/runs/run_8f2k3m/actions/approve \ -H "Authorization: Bearer $STACKSHIP_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d @approval_binding.json
If the plan or its policy results changed since you read the review, you get
409 APPROVAL_STALE; read the review again. Approving requires a role with
approval permission; see Run approvals.
Cancel a run
curl -X POST https://api.stackship.run/v1/runs/run_8f2k3m/actions/cancel \
-H "Authorization: Bearer $STACKSHIP_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"Canceling an apply stops OpenTofu at a safe point, which can take up to 15 minutes. Once that cancel is pending, force cancel to stop immediately:
curl -X POST https://api.stackship.run/v1/runs/run_8f2k3m/actions/force-cancel \
-H "Authorization: Bearer $STACKSHIP_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"Force cancel requires the same permission as cancel. Without a pending cancel
on an applying run, it returns 409 RUN_FORCE_CANCEL_NOT_AVAILABLE. A force
canceled apply needs recovery.
Terraform Cloud–compatible API
OpenTofu’s cloud block and Terraform Cloud tools, such as the tfe provider,
use the Terraform Cloud–compatible API under /api/v2. It follows Terraform
Cloud’s behavior:
- Run and workspace lists are paginated with
page[number]andpage[size](20 items by default, up to 100). search[name]matches workspace names that contain the value, ignoring case.search[wildcard-name]accepts a leading or trailing*, for examplenetwork-*or*-prod. Aremotebackend that selects workspaces withworkspaces { prefix = "..." }works. Thecloudblock has noprefix; it selects workspaces bynameortags.GET /api/v2/workspaces/{workspace_id}reads a workspace by its ID.- An action that conflicts with a run’s current state, such as approving a run
that’s no longer waiting, returns
409. - Canceling or discarding a run that someone else already canceled or
discarded returns
202and changes nothing. - Log URLs returned for a plan or apply stay valid for 6 hours.
- A rejected state upload, for example one whose serial, lineage, or checksum
doesn’t match the state version it was created for, returns
422with the reason in the error’stitle. PATCH /api/v2/vars/{variable_id}keeps a variable’s sensitivity when you leave outsensitive.value,description, andhclare optional and changeable.keyandcategoryare accepted only when unchanged; changing either returns422, so create a new variable instead. Changing a sensitive variable to non-sensitive returns422; delete and recreate it instead.- A plan’s JSON output (
/api/v2/plans/{plan_id}/json-output) contains sensitive values, so it requires permission to read state and sensitive run artifacts (owner or admin). Other callers, including workspace tokens, get404. Each read is recorded in the audit log.
Limits
| Limit | Value |
|---|---|
| JSON request body | 64 KB (128 KB for variables) |
| Variable value | 64 KB |
| Page size | 100 items |
| State file or run artifact | 100 MB |
| Idempotency key retention | 24 hours |
| Access token lifetime | 1 hour |