TFE V2 CLI Surface

September 17, 2026 · View on GitHub

This file enumerates the exact set of TFE V2 API endpoints that the terraform (HashiCorp) and tofu (OpenTofu) CLIs consume — directly or via go-tfe — when configured with a cloud block (or the legacy remote backend) pointing at a TFE-compatible server.

This list is the contract for /api/tfe/v2/ in Terrapod. A route on this list MUST be served at /api/tfe/v2/... so the CLI can find it via service discovery. A route NOT on this list — even one defined in the public TFE V2 spec — belongs at /api/v1/.... The cleanup rule:

If terraform/tofu doesn't call it, the route is Terrapod-native, regardless of TFE-V2 lineage.

Verification source: OpenTofu internal/cloud and internal/backend/remote packages, cross-referenced against go-tfe client.NewRequest(...) paths.

Discovery / Auth

MethodPath
GET/.well-known/terraform.json

The discovery document points at:

  • tfe.v2 — the TFE V2 API base. Discovered, not fixed: terraform and tofu both honour whatever path is advertised here (verified with a real init/plan/apply in #1528), which is what allowed this surface to move off /api/v2. The old path is still served for clients that cache discovery.
  • modules.v1 — module registry CLI download protocol
  • providers.v1 — provider registry CLI download protocol
  • login.v1terraform login OAuth2 endpoints (/oauth/authorize, /oauth/token)

Organizations

MethodPathgo-tfe methodCaller
GET/api/tfe/v2/pingclient initgo-tfe configures every connection
GET/api/tfe/v2/account/detailsAccount.Readgo-tfe startup
GET/api/tfe/v2/organizations/defaultOrganizations.Readcloud backend init
GET/api/tfe/v2/organizations/default/entitlement-setOrganizations.ReadEntitlementscloud/backend.go:349, remote/backend.go:362
GET/api/tfe/v2/organizations/default/runs/queueOrganizations.ReadRunQueuecloud/backend_common.go:170, remote/backend_common.go:172 (run-status display)
GET/api/tfe/v2/organizations/default/capacityOrganizations.ReadCapacitycloud/backend_common.go:193, remote/backend_common.go:195

Projects

cloud block only — the project argument routes workspaces into projects.

MethodPathgo-tfe methodCaller
GET/api/tfe/v2/organizations/default/projectsProjects.Listcloud/backend.go:588, 676 (returns 422 — see below)
POST/api/tfe/v2/organizations/default/projectsProjects.Createcloud/backend.go:715 (returns 422 — see below)

Terrapod is single-organization with no project concept. Both endpoints return 422 Projects are not supported with a JSON:API error directing the caller to omit the project argument from the cloud block. See docs/getting-started.md. Label-based RBAC (docs/rbac.md) covers the same scoping use case projects served in TFC, and is more flexible — a workspace can match on any combination of label dimensions instead of a single project, and the same labels drive UI filtering of workspaces and other resources.

Workspaces

MethodPathgo-tfe methodCaller
GET/api/tfe/v2/organizations/default/workspacesWorkspaces.Listcloud/backend.go:601, 1175, remote/backend.go:488
POST/api/tfe/v2/organizations/default/workspacesWorkspaces.Createcloud/backend.go:726, remote/backend.go:591
GET/api/tfe/v2/organizations/default/workspaces/{name}Workspaces.Readcloud/backend.go:635, 661, 1120, 1151, cloud/backend_common.go:95, cloud/backend_context.go:178, remote/backend.go:575, 643, remote/backend_context.go:181
DELETE/api/tfe/v2/organizations/default/workspaces/{name}Workspaces.Deleteremote/backend_state.go:145
GET/api/tfe/v2/workspaces/{id}(read by id)go-tfe pattern
PATCH/api/tfe/v2/workspaces/{id}Workspaces.UpdateByIDcloud/backend.go:739
POST/api/tfe/v2/workspaces/{id}/relationships/tagsWorkspaces.AddTagscloud/backend.go:760
DELETE/api/tfe/v2/workspaces/{id}/relationships/tagsWorkspaces.RemoveTagstag-binding maintenance
GET/api/tfe/v2/workspaces/{id}/tag-bindingsWorkspaces.ListTagBindingstag display
GET/api/tfe/v2/workspaces/{id}/effective-tag-bindingsWorkspaces.ListEffectiveTagBindingstag display
POST/api/tfe/v2/workspaces/{id}/actions/lockWorkspaces.Lockremote/backend_state.go:164
POST/api/tfe/v2/workspaces/{id}/actions/unlockWorkspaces.Unlockremote/backend_state.go:201
POST/api/tfe/v2/workspaces/{id}/actions/force-unlockWorkspaces.ForceUnlockremote/backend_state.go:222
GET/api/tfe/v2/workspaces/{id}/runsRuns.Listcloud/backend_common.go:119, remote/backend_common.go:121
GET/api/tfe/v2/workspaces/{id}/varsVariables.Listcloud/backend_context.go:122, remote/backend_context.go:123 (read-only — sensitive-var warnings)

Note: DELETE /api/tfe/v2/workspaces/{id} is not in the CLI surface. Only the by-name delete is. The by-id delete is admin/UI-only and lives on the management API.

State Versions

MethodPathgo-tfe methodCaller
GET/api/tfe/v2/workspaces/{id}/current-state-versionStateVersions.ReadCurrentremote/backend_state.go:40
POST/api/tfe/v2/workspaces/{id}/state-versionsStateVersions.Createremote/backend_state.go:85
GET/api/tfe/v2/state-versions/{id}StateVersions.Readfollow-up reads
PUT/api/tfe/v2/state-versions/{id}/content(raw upload to upload-url)remote/backend_state.go:129no Authorization header
PUT/api/tfe/v2/state-versions/{id}/json-content(raw upload)same flow, JSON state
GET/api/tfe/v2/state-versions/{id}/downloadStateVersions.Downloadremote/backend_state.go:49 (follows download-url from resource)

The upload-url and download-url returned in JSON:API attributes can be absolute or server-relative — go-tfe's NewRequest handles both. Terrapod returns relative /api/tfe/v2/... paths.

Configuration Versions

MethodPathgo-tfe methodCaller
POST/api/tfe/v2/workspaces/{id}/configuration-versionsConfigurationVersions.Createcloud/backend_plan.go:135, remote/backend_plan.go:206
GET/api/tfe/v2/configuration-versions/{id}ConfigurationVersions.Readcloud/backend_plan.go:199, remote/backend_plan.go:270
PUT/api/tfe/v2/configuration-versions/{id}/upload(raw upload to upload-url)cloud/backend_plan.go:186, remote/backend_plan.go:257no Authorization header

Terrapod-only management on the configuration-versions surface (list, download, diff, ticket-based download) lives at /api/v1/configuration-versions/....

Runs / Plans / Applies

MethodPathgo-tfe methodCaller
POST/api/tfe/v2/runsRuns.Createcloud/backend_plan.go:278, remote/backend_plan.go:323
GET/api/tfe/v2/runs/{id}Runs.Read / ReadWithOptionsmany — every run-status poll
POST/api/tfe/v2/runs/{id}/actions/applyRuns.Applycloud/backend_apply.go:197, remote/backend_apply.go:253
POST/api/tfe/v2/runs/{id}/actions/discardRuns.Discardcloud/backend_common.go:519, remote/backend_apply.go:208
POST/api/tfe/v2/runs/{id}/actions/cancelRuns.Cancelcloud/backend.go:934, remote/backend.go:818
GET/api/tfe/v2/runs/{id}/run-eventsRunEvents.Listrun progress polling
GET/api/tfe/v2/plans/{id}Plans.Readrun status
GET/api/tfe/v2/plans/{id}/logPlans.Logs (via log-read-url)cloud/backend_plan.go:428, remote/backend_plan.go:380
GET/api/tfe/v2/plans/{id}/json-outputPlans.ReadJSONOutputcloud/backend_show.go:68 (302 → presigned storage URL; advertised via json-output attribute on the plan when present)
GET/api/tfe/v2/applies/{id}Applies.Readrun status
GET/api/tfe/v2/applies/{id}/logApplies.Logs (via log-read-url)cloud/backend_apply.go:229, remote/backend_apply.go:272

Cost Estimates / Policy Checks / Task Stages

These are CLI-aware (run progress display branches on relationships) but only exercised when the relationship is present on the run. Terrapod does not implement cost estimates in this shape, or Sentinel. Its OPA policy sets and security scan are served as policy checks, and run tasks as task stages (#1704).

The run lists both in its relationships only in the Terraform Enterprise post-plan vocabulary (api.config.runs.tfe_post_plan_decisions, the default from 2.0), because that is what makes the CLI act on them; the same setting makes a held run report policy_override / post_plan_awaiting_decision, the statuses on which the CLI's confirm loop offers an override. See post-plan-decisions.md.

MethodPathgo-tfe methodCaller
GET/api/tfe/v2/runs/{id}?include=task_stagesRuns.ReadWithOptionscloud/backend_taskStages.go:42
GET/api/tfe/v2/task-stages/{id}TaskStages.Readcloud/backend_taskStages.go:66, 96
POST/api/tfe/v2/task-stages/{id}/actions/overrideTaskStages.Overridecloud/backend_taskStages.go:186
GET/api/tfe/v2/policy-checks/{id}PolicyChecks.Readcloud/backend_common.go:326
GET/api/tfe/v2/policy-checks/{id}/outputPolicyChecks.Logscloud/backend_common.go:319
POST/api/tfe/v2/policy-checks/{id}/actions/overridePolicyChecks.Overridecloud/backend_common.go:394, 411
GET/api/tfe/v2/runs/{id}/policy-checksPolicyChecks.Listnot called by the CLI; served for go-tfe clients

The run-task management surface (/run-tasks/*, /workspaces/{id}/run-tasks, callback endpoints) is Terrapod-native and lives at /api/v1/.

tfci / tfc-workflows-github

tfci is HashiCorp's CI binary; tfc-workflows-github wraps it for GitHub Actions. It is in widespread use for TFE/HCP-Terraform CI flows. Most of what it calls overlaps with the CLI surface above (workspace lookup, lock/unlock, configuration-version create+upload, run create/apply/discard/cancel/read, plan read+log+JSON output). The one extension beyond the terraform/tofu CLI surface is variable managementtfci variable … and tfci variable-set … commands.

We extend the "stays at /api/tfe/v2/" set to cover those calls. The rule is unchanged: anything terraform, tofu, or tfci calls stays at /api/tfe/v2/; everything else is /api/v1/.

MethodPathgo-tfe methodCaller
POST/api/tfe/v2/workspaces/{id}/varsVariables.Createtfci variable create
PATCH/api/tfe/v2/workspaces/{id}/vars/{id}Variables.Updatetfci variable update
DELETE/api/tfe/v2/workspaces/{id}/vars/{id}Variables.Deletetfci variable delete
GET/api/tfe/v2/organizations/default/varsetsVariableSets.Listtfci variable-set list
POST/api/tfe/v2/organizations/default/varsetsVariableSets.Createtfci variable-set create
GET/api/tfe/v2/varsets/{id}VariableSets.Readtfci variable-set show
PATCH/api/tfe/v2/varsets/{id}VariableSets.Updatetfci variable-set update
DELETE/api/tfe/v2/varsets/{id}VariableSets.Deletetfci variable-set delete
GET/api/tfe/v2/varsets/{id}/relationships/varsVariableSetVariables.Listtfci variable-set ...
POST/api/tfe/v2/varsets/{id}/relationships/varsVariableSetVariables.Createtfci variable-set add-variable
PATCH/api/tfe/v2/varsets/{id}/relationships/vars/{id}VariableSetVariables.Updatetfci variable-set update-variable
DELETE/api/tfe/v2/varsets/{id}/relationships/vars/{id}VariableSetVariables.Deletetfci variable-set delete-variable
POST/api/tfe/v2/varsets/{id}/relationships/workspacesVariableSets.ApplyToWorkspacestfci variable-set assign-to-workspace
DELETE/api/tfe/v2/varsets/{id}/relationships/workspacesVariableSets.RemoveFromWorkspacestfci variable-set remove-from-workspace

Verification: When updating this section, check the tfci source at https://github.com/hashicorp/tfc-workflows-tooling — the internal/cloud/ package exposes the call sites.

Out of scope for tfci compat: teams, projects, run tasks (TFE-shape), notifications, OAuth client management, the hashicorp/tfe Terraform provider's full surface. Those have structural divergence in Terrapod (single-org, label-RBAC instead of teams) that compatibility cannot bridge.

Module Registry (CLI Download Protocol)

Service-discovered via modules.v1 URL.

MethodPath
GET/api/tfe/v2/registry/modules/{namespace}/{name}/{provider}/versions
GET/api/tfe/v2/registry/modules/{namespace}/{name}/{provider}/{version}/download

The download endpoint returns 204 with X-Terraform-Get header pointing at the actual tarball URL.

Provider Registry (CLI Download Protocol)

Service-discovered via providers.v1 URL.

MethodPath
GET/api/tfe/v2/registry/providers/{namespace}/{name}/versions
GET/api/tfe/v2/registry/providers/{namespace}/{name}/{version}/download/{os}/{arch}

The platform-download endpoint returns JSON with download_url, shasums_url, and shasums_signature_url — the CLI then follows those URLs (which may be absolute, presigned, or server-relative).

Maintaining This Document

When OpenTofu adds new go-tfe call sites in internal/cloud/* or internal/backend/remote/*, this list grows. Re-verify by:

  1. git clone https://github.com/opentofu/opentofu
  2. grep -RP 'tfe\.[A-Z]\w+\(' internal/cloud internal/backend/remote --include='*.go' | grep -v _test.go
  3. For each match, find the go-tfe method's client.NewRequest(...) line in https://github.com/hashicorp/go-tfe to extract the path
  4. Add to the relevant table above

Endpoints not verified by this process belong at /api/v1/, regardless of go-tfe lineage.