Skip to content

v1 REST API

The frozen native StackShip dashboard contract, authentication rules, idempotency, and streaming operations.

Updated View as Markdown

The native API base URL is:

https://api.stackship.run/v1

Its reproducible OpenAPI 3.1 document is served at /v1/openapi.json and committed at apps/api/openapi/stackship-v1.json. Release verification regenerates the artifact and fails on drift.

OpenTofu uses the separate native cloud compatibility surface under /api/v2. Do not use /v1 as an OpenTofu backend.

The compatibility surface returns the same 404 JSON:API error document for missing resources and tenant resources the active credential cannot access, including run actions. Workspace documents expose current-run as the newest non-terminal run in that workspace; the relationship is null when no run is active. Every compatibility error returns a canonical X-Request-Id header and repeats that exact value in errors[0].meta.request_id; StackShip replaces a missing or invalid client-supplied request ID. Native v1 workspace records expose cloud_name separately from the human name and URL-safe slug; use cloud_name as the OpenTofu cloud workspace selector. Workspace creation accepts lowercase URL slugs up to 90 characters so the initialized cloud_name always satisfies the native cloud contract.

Authentication and tenancy

Native /v1 product routes require an authenticated session, a selected active organization, and a live membership. StackShip derives the tenant from that membership and repeats the organization constraint in every repository query. A request body, path, or query parameter cannot select an authorization tenant.

One bearer credential is additionally accepted: an MCP-audience OAuth access token, presented in the Authorization header, authenticates the MCP automation surface. The API resolves it against its hashed token record — so revocation applies immediately — requires the mcp:tools scope, the MCP resource audience, and the token’s organization claim, and then applies the same live-membership tenant derivation as a session. MCP bearer sessions are confined to the published MCP operation list, need mcp:write for every mutation, and never satisfy step-up reauthentication. A bearer request never falls back to cookie authentication, and OpenTofu service-account (sst_) and run (rat_) tokens remain rejected on /v1.

Missing authentication returns 401; a stale membership or missing permission returns 403. Protected operations fail closed when the identity database is unavailable.

The dashboard calls /v1 through its same-origin /api/control-plane proxy. That proxy admits only generated-contract method/path pairs, rewrites only the host dashboard session cookie upstream, rejects unsafe cross-origin mutations, and does not expose the API session cookie to browser JavaScript. The production OpenAPI security scheme names the upstream __Secure-better-auth.session_token cookie accepted by api.stackship.run; dashboard integrations should still use the same-origin proxy instead of handling that cookie directly.

Implemented operations

The committed contract is the authority. Its operation groups are:

Area Operations
Projects list, create, and read
Workspaces list, create, read, update exact execution settings and display name
Runs create, organization/workspace lists, read, cancel
Review sanitized evidence with the recorded decision actor, approve, reject, ordered log stream with a bounded tail read, ordinary artifact stream, reason-bearing sensitive artifact download
State immutable workspace history and restricted, reason-bearing raw-state download
Variables metadata list, create, replace (value optional for metadata-only edits), delete
Audit newest sanitized organization events
Policy sets, streaming source publication, immutable versions, workspace attachment inventory and detach, results, soft override
GitHub App install flow, installation status, repository list, source settings, signed webhook receiver
Registry module list, create, read, and delete; source update; tag sync; version and ingestion lists; version deprecate and restore
Credentials workspace profiles, immutable versions, verification, trust template
OpenTofu access service-account creation, one-time token issuance, revocation
Toolchains exact supported OpenTofu version list

PATCH /workspaces/{workspaceId} accepts an optional name alongside the required execution settings. A rename never changes the workspace slug or cloud_name, and a display name already used by another workspace in the organization returns 409 WORKSPACE_NAME_TAKEN — the same code creation uses.

PUT /workspace-variables/{variableId} accepts an optional hcl flag and makes value optional. Omitting value keeps the stored value, version, and digest untouched, so a sensitive value is never re-encrypted or re-exposed by a description- or hcl-only edit. When value is omitted, sensitive must equal the variable’s current sensitivity; changing sensitivity without a new value returns 400 WORKSPACE_VARIABLE_VALUE_REQUIRED.

GET /runs/{runId}/logs additionally accepts tail=<n> (1–100) to return only the newest n sanitized log chunks, still in ascending sequence order. tail is mutually exclusive with after and limit; the X-StackShip-Log-Last-Sequence header still names the newest returned sequence so a client can continue forward polling with after.

The run review carries a decision object once a human decision exists: action (approved, rejected, or canceled), the acting principal’s actor_id, a display-safe actor_name resolved through the identity user table (null when the principal no longer resolves), and decided_at. An approved run names the approval record’s author; a rejected or canceled run names the user-action signal’s actor. Reviews without a human decision, including approval-deadline expiries, carry decision: null.

GET /github/installation reports the active organization’s newest GitHub App installation as connected, suspended, or not_installed, with its installation_id, account_login, and connected_at when one exists. Revoked installations read as not_installed.

The committed OpenAPI contract includes the /v1/registry management operations for the private module registry: modules, repository sources, tag sync, versions, ingestion history, and version deprecation and restoration. Module consumption itself uses the separate modules.v1 protocol app, not /v1.

The following are not available today through native v1: billing, drift detection, customer-operated runners, general notifications, and auto-apply. Run creation rejects an auto_apply field rather than silently ignoring it.

Idempotency

Every state-changing POST, PUT, PATCH, and DELETE operation in the committed native v1 contract requires Idempotency-Key, except the signed GitHub webhook receiver. Sensitive download routes also use POST so their operator reason stays out of the URL, but are read operations and do not use an idempotency key. They reject every non-empty query string before reading artifact or state storage; the reason belongs only in the bounded form body. A key contains 1–255 visible ASCII characters. StackShip hashes the canonical parsed request and stores the fingerprint with the exact response status and serialized domain body. An exact retry receives that original result inside a fresh request envelope; meta.request_id identifies the current HTTP attempt and therefore differs between an initial response and its replay. Reusing the key with different input returns 409 IDEMPOTENCY_KEY_REUSED.

A stored key expires 24 hours after it is first reserved. Once it expires the key no longer replays, and the next request that presents it executes as a new mutation. Presenting a key that another in-flight request already reserved, but has not yet completed, returns 409 IDEMPOTENCY_IN_PROGRESS; retry after the first request settles.

For generic-secret version creation, the secret environment map is first reduced to a domain-separated HMAC under the API-only IDEMPOTENCY_HMAC_KEY. The ordinary request fingerprint contains that keyed value and public metadata, never plaintext or an offline-guessable plaintext hash. The endpoint returns 503 before sealing or persistence when the key is missing or weak.

The idempotency reservation, domain mutation, audit evidence, durable outbox entry when needed, and serialized response commit in one PlanetScale transaction. A rollback leaves none of them committed. Replaying a completed public request never calls a credential broker again, mints a second token, or directly dispatches another Workflow.

The webhook receiver instead uses the signed X-GitHub-Delivery value, provider event identity, and payload digest as its natural retry identity. An exact delivery replay returns the recorded safe ingestion IDs and does not dispatch a Workflow from the request path. A changed identity or digest conflicts.

Mutation Retry identity
POST /projects Idempotency-Key
POST /workspaces Idempotency-Key
PATCH /workspaces/{workspaceId} Idempotency-Key
POST /runs Idempotency-Key
POST /runs/{runId}/actions/approve Idempotency-Key plus exact review binding
POST /runs/{runId}/actions/reject Idempotency-Key plus exact review binding
POST /runs/{runId}/actions/cancel Idempotency-Key
POST /runs/{runId}/actions/recover Idempotency-Key, recent step-up, exact state and emergency evidence
POST /workspaces/{workspaceId}/variables Idempotency-Key
PUT /workspace-variables/{variableId} Idempotency-Key
DELETE /workspace-variables/{variableId} Idempotency-Key
POST /policy-sets Idempotency-Key
POST /policy-sets/{policySetId}/versions Idempotency-Key
POST /policy-sets/{policySetId}/attachments Idempotency-Key
POST /workspaces/{workspaceId}/policy-sets/{policySetId}/versions Idempotency-Key plus the exact source stream
POST /workspaces/{workspaceId}/policy-attachments Idempotency-Key
DELETE /workspaces/{workspaceId}/policy-attachments/{attachmentId} Idempotency-Key
POST /runs/{runId}/policy-overrides Idempotency-Key
POST /github/installations/start Idempotency-Key
PUT /workspaces/{workspaceId}/source Idempotency-Key
POST /registry/modules Idempotency-Key
DELETE /registry/modules/{moduleId} Idempotency-Key
POST /registry/modules/{moduleId}/actions/sync Idempotency-Key
PUT /registry/modules/{moduleId}/source Idempotency-Key
POST /registry/module-versions/{versionId}/actions/deprecate Idempotency-Key
POST /registry/module-versions/{versionId}/actions/restore Idempotency-Key
POST /workspaces/{workspaceId}/credential-profiles Idempotency-Key
POST /credential-profiles/{profileId}/actions/archive Idempotency-Key plus an exact empty object
POST /credential-profiles/{profileId}/versions Idempotency-Key
POST /credential-profiles/{profileId}/versions/{versionId}/actions/revoke Idempotency-Key plus an exact empty object
POST /credential-profiles/{profileId}/versions/{versionId}/actions/verify Idempotency-Key
POST /service-accounts Idempotency-Key
POST /service-accounts/{serviceAccountId}/tokens Idempotency-Key
DELETE /service-accounts/{serviceAccountId}/tokens/{tokenId} Idempotency-Key
POST /github/webhooks Signed X-GitHub-Delivery and payload identity

Generic-secret profile versions reject any environment value containing a NUL code point with CREDENTIAL_SECRET_VALUE_INVALID before the plaintext reaches the sealing service. Broker and runner boundaries independently reject the same invalid value if it crosses an internal boundary.

The GitHub installation callback is a provider-mandated state-changing GET. Its signed, expiring state digest is single use and bound to the organization and initiating principal.

Approval and rejection additionally carry the current plan, dependency, predecessor-state, and policy-result binding. A stale binding is rejected instead of authorizing a changed plan. The review advertises neither action until that exact binding exists. A closed policy gate may advertise rejection but never approval; unexpected gate-check failures surface instead of being reported as ordinary policy blocks.

Apply recovery accepts current_state_verified or import_emergency_state, the displayed current state id and digest, the displayed emergency-state digest when importing, a reason, and confirm_fence_advance: true. It requires both recovery permissions and recent step-up. A prepared operation is durable and resumes the same recovery and target-state identities after response loss; changed evidence conflicts. It leases its principal for 15 minutes. Before the lease expires, no caller can take it over. After expiry, a separately authorized caller must resubmit the same resolution and state/emergency evidence with confirm_takeover: true; StackShip reuses the recovery and target-state identities, atomically fences the previous principal, records bounded takeover audit evidence, and never retries the apply. The run review represents an active prepared lease with lease_expires_at and lease_remaining_seconds. The remaining value is derived from the PostgreSQL clock and is always an integer from 0 through 900; both fields are null when no prepared lease is active. The global apply switch does not block this containment path.

Service-account creation and issuance are separate native operations. The dashboard retains the non-secret service-account id after a failed issuance so a retry does not create another account. Issuance requires an explicit permission subset, optional project/workspace boundaries, and an expiry no more than 365 days away. The response is the only operation that returns token plaintext; no service-account or token list operation is exposed in the v1 contract.

Token issuance is safe across response loss without storing bearer plaintext. The exact authenticated principal, service account, request, and idempotency key deterministically derive the same token; PlanetScale stores only its digest and a metadata-only replay body. An exact retry re-derives the same disclosure. A changed request with the same key conflicts.

Deferred Workflow delivery

Run, source-ingestion, registry-ingestion, and policy-publication mutations commit a bounded Workflow-dispatch outbox record before returning. The API attempts immediate delivery after commit. Failure leaves the public result intact and the outbox record pending.

Every five minutes, the internal orchestrator calls an authenticated API reconciler. One tick claims at most 25 due records for 60 seconds. Claims, completion, and retry release are constrained by organization, dispatch key, and claim owner. Failures use exponential backoff from five seconds to five minutes. Workflow IDs are deterministic, and create-response loss is reconciled through get before completion, so a retry cannot create a second logical Workflow.

Response boundary

The generated dashboard client sends credentials, propagates the request id, refuses redirects for JSON operations, and validates exact response schemas. An ApiError retains only status, code, and request id; response bodies, headers, URLs, and credential material are not retained.

Most successful JSON operations use:

{
  "data": {
    "id": "run_example",
    "object": "run"
  },
  "meta": {
    "request_id": "req_example"
  }
}

Errors carry a bounded error object and the same request metadata.

Cursor-backed lists

Organization runs, audit events, and workspace state versions use one opaque forward-pagination contract. Supply the endpoint’s returned next_cursor as the next request’s cursor; do not inspect it or reuse it with another endpoint. The optional limit is an integer from 1 through 100.

GET /v1/runs?limit=50&cursor=v1.opaque-token

These list responses keep the array in data and add page metadata:

{
  "data": [],
  "meta": {
    "request_id": "req_example",
    "page": {
      "next_cursor": null
    }
  }
}

A non-null next_cursor is the only continuation signal. There is no public has_more flag and records do not carry their own cursor. StackShip orders each list by creation time and a stable identity tie-breaker, so records with the same timestamp cross page boundaries exactly once. Those ordering columns are stored at millisecond precision, matching the timestamp carried by the cursor, so PostgreSQL cannot retain a finer instant that the next-page query cannot represent. A malformed, over-sized, outdated, or endpoint-mismatched token returns the endpoint’s *_LIST_CURSOR_INVALID error with HTTP 400.

Streaming limits

Run logs, state files, and run artifacts are not parsed into Worker-sized buffers. The API and dashboard proxy forward ReadableStream bodies with backpressure.

  • State and artifact plaintext is limited to 100_000_000 bytes.
  • Configuration archive uploads use the same 100 MB launch ceiling.
  • Policy source publication accepts an application/gzip .tar.gz body and uses the same 100 MB ceiling. The dashboard passes the browser File through unchanged; neither Worker aggregates it in memory.
  • Ordinary JSON control documents are limited to 65_536 bytes. Variable mutation envelopes are limited to 131_072 bytes so one supported 65_536-byte value still fits with its bounded metadata. Credential-profile mutation documents are limited to 100_000 bytes so a supported 64 KiB canonical generic-secret environment map fits inside its bounded version envelope. Oversized documents receive 413 PAYLOAD_TOO_LARGE; malformed JSON receives 400 VALIDATION_FAILED.
  • Restricted download reasons use a form body bounded to 4 KiB and a trimmed reason of 1–500 characters. Raw state additionally requires state:read; every restricted download requires artifact:read_sensitive, step-up reauthentication no older than 15 minutes, and a pre-stream audit record.
  • The API rejects over-limit declared sizes and aborts on an over-limit stream.
  • R2 plaintext URLs are never returned to the browser.
  • A state-download audit record is committed before the response body is released.

Credential profiles

Credential profile versions are immutable and phase scoped. The current generated contract covers AWS, Azure, and Google Cloud federation profiles, plus write-only generic environment secrets.

Create the profile with provider: "generic_secret". Create a version with:

{
  "audience": "stackship:generic-secret",
  "phase": "plan_and_apply",
  "configuration": {
    "environment": {
      "VENDOR_TOKEN": "write-only-value"
    }
  }
}

New generic-secret versions require phase: "plan_and_apply". New cloud federation versions may use plan_and_apply or speculative_plan. The API rejects standalone plan and apply creation with 400 CREDENTIAL_PROFILE_PHASE_UNSUPPORTED; generic secrets also reject speculative_plan with that code. The environment map accepts 1–32 safe, unique names and at most 64 KiB of canonical UTF-8 JSON. Profile creation and version writes require credential-write authority. Credential-profile mutation documents are capped at 100,000 bytes before parsing; the difference between that envelope limit and the 64 KiB map limit is reserved for bounded profile metadata and JSON framing.

A newly created profile remains a detached draft until a version is verified and activated as current. Detached drafts are omitted from run snapshots. If a current version exists but is not verified or does not permit the requested run, snapshotting fails closed.

Credential writers can archive a profile or revoke an exact version without deleting its history. Archive sets archived_at and detaches the complete profile. Revoke sets the selected version to disabled, records disabled_at, and clears its current marker. The revoke API accepts current and historical versions for incident response; the dashboard initially exposes the control only for the current verified version.

Both actions require credential:write, an Idempotency-Key, and an exact empty JSON object. A cross-tenant or profile/version-mismatched identifier returns 404 NOT_FOUND. A new request against an already transitioned resource returns 409 CREDENTIAL_PROFILE_ARCHIVED or 409 CREDENTIAL_PROFILE_VERSION_DISABLED. Successful transitions append credential_profile.archived or credential_profile_version.revoked audit events with non-secret identifiers and provider metadata.

Run launch and runner redemption re-read persisted credential lifecycle state. Consequently, an archive or revoke blocks new snapshots and also makes an already queued snapshot fail credential issuance or redemption. StackShip does not delete the immutable version, encrypted envelope, or historical run evidence.

The create, list, and verify response never echoes the submitted map or its envelope. A generic version instead has:

{
  "provider": "generic_secret",
  "audience": "stackship:generic-secret",
  "configuration": null,
  "environment_names": ["VENDOR_TOKEN"]
}

Verification asks the internal broker to decrypt and validate the stored envelope before activation. The trust-template operation is not available for generic profiles. The public API has no secret-read or handle-redemption operation; redemption is an internal, authenticated runner operation with an exact-bound, one-time capability.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close