Terrapod implements the TFE V2 API for compatibility with the terraform CLI, go-tfe client, and existing CI/CD integrations. All endpoints use JSON:API format.
The interactive API documentation is also available in the web UI under API in the navigation bar, offering both ReDoc and Swagger UI views.
All API endpoints are prefixed with /api/v2. Example:
https://terrapod.example.com/api/v2/organizations/default/workspaces
Include a Bearer token in the Authorization header:
Authorization: Bearer <api-token-or-session-token>
API tokens are obtained via terraform login, the web UI, or the token creation endpoint. Session tokens are obtained via the login flow.
Requests with a body should use:
Content-Type: application/vnd.api+json
Responses use application/json (accepted by go-tfe).
Terrapod uses a single hardcoded organization: default. The {org} path parameter is accepted for CLI compatibility but only default is valid.
GET /health
Returns 200 if the API process is running.
Response:
{"status": "healthy"}GET /ready
Checks database, Redis, and storage subsystems. Returns 200 if all healthy, 503 otherwise.
Response (healthy):
{
"status": "ready",
"checks": {
"database": "healthy",
"redis": "healthy",
"storage": "healthy"
}
}GET /.well-known/terraform.json
Returns service discovery document for terraform login and registry protocol.
Response:
{
"login.v1": {
"client": "terraform-cli",
"grant_types": ["authz_code"],
"authz": "/oauth/authorize",
"token": "/oauth/token",
"ports": [10000, 10010]
},
"modules.v1": "/api/v2/registry/modules/",
"providers.v1": "/api/v2/registry/providers/"
}GET /api/v2/ping
API version handshake. Returns TFE-compatible version headers.
Response headers:
TFP-API-Version: 2.6
TFP-AppName: Terrapod
X-TFE-Version: v0.1.0
GET /api/v2/account/details
Returns the authenticated user's information.
Response:
{
"data": {
"id": "user-abc123",
"type": "users",
"attributes": {
"username": "alice@example.com",
"email": "alice@example.com",
"is-service-account": false
}
}
}GET /api/v2/organizations/{org}
Returns organization details. Only default is valid.
GET /api/v2/organizations/{org}/entitlement-set
Returns feature flags (all enabled for Terrapod).
GET /api/v2/organizations/{org}/workspaces
GET /api/v2/organizations/{org}/workspaces/{name}
GET /api/v2/workspaces/{id}
POST /api/v2/organizations/{org}/workspaces
Request body:
{
"data": {
"type": "workspaces",
"attributes": {
"name": "my-workspace",
"auto-apply": false,
"execution-mode": "remote",
"terraform-version": "1.9.8",
"resource-cpu": "1",
"resource-memory": "2Gi",
"labels": {
"env": "dev",
"team": "platform"
},
"vcs-repo-url": "https://github.com/org/repo",
"vcs-branch": "main",
"vcs-working-directory": "terraform/",
"drift-detection-enabled": false,
"drift-detection-interval-seconds": 86400
},
"relationships": {
"vcs-connection": {
"data": {
"id": "vcs-abc123",
"type": "vcs-connections"
}
}
}
}
}Required permission: Any authenticated user can create workspaces (creator becomes owner).
PATCH /api/v2/workspaces/{id}
Same body format as create. Only include attributes to change.
Required permission: admin on the workspace.
Self-lockout protection: If the request changes labels and the new labels would reduce the caller's own access level, the API returns 409 Conflict with a descriptive error. Re-submit with "force": true in the attributes to confirm the change. Platform admins and workspace owners are immune (their access doesn't depend on labels).
All workspace responses (show and list) include a permissions object reflecting the authenticated user's effective permissions:
{
"permissions": {
"can-update": true,
"can-destroy": true,
"can-queue-run": true,
"can-read-state-versions": true,
"can-create-state-versions": true,
"can-read-variable": true,
"can-update-variable": true,
"can-lock": true,
"can-unlock": true,
"can-force-unlock": true,
"can-read-settings": true
}
}
### Delete Workspace
DELETE /api/v2/workspaces/{id}
**Required permission:** `admin` on the workspace.
### Lock Workspace
POST /api/v2/workspaces/{id}/actions/lock
**Required permission:** `plan` on the workspace.
### Unlock Workspace
POST /api/v2/workspaces/{id}/actions/unlock
**Required permission:** `plan` on the workspace (own locks only).
### Drift Detection Attributes
Workspaces support the following drift detection attributes (settable on create and update):
| Attribute | Type | Default | Description |
|---|---|---|---|
| `drift-detection-enabled` | boolean | `false` | Enable or disable automatic drift detection for this workspace |
| `drift-detection-interval-seconds` | integer | `86400` | How often to run drift detection checks (minimum: 3600 seconds / 1 hour) |
The following read-only attributes are included in workspace responses when drift detection is enabled:
| Attribute | Type | Description |
|---|---|---|
| `drift-last-checked-at` | string (RFC3339) or null | Timestamp of the last completed drift detection check |
| `drift-status` | string | Current drift status: `""` (never checked), `"no_drift"`, `"drifted"`, or `"errored"` |
---
## State Versions
### List State Versions
GET /api/v2/workspaces/{id}/state-versions
**Required permission:** `read` on the workspace.
### Current State Version
GET /api/v2/workspaces/{id}/current-state-version
**Required permission:** `read` on the workspace.
### Create State Version
POST /api/v2/workspaces/{id}/state-versions
**Request body:**
```json
{
"data": {
"type": "state-versions",
"attributes": {
"serial": 1,
"md5": "d41d8cd98f00b204e9800998ecf8427e",
"lineage": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
}
Required permission: write on the workspace.
GET /api/v2/state-versions/{id}
GET /api/v2/state-versions/{id}/download
Returns a redirect to a presigned URL for the raw state file.
Required permission: plan on the workspace.
PUT /api/v2/state-versions/{id}/content
Binary upload of raw state bytes. No auth required (presigned-style -- the state version UUID acts as a capability token). This matches go-tfe behavior.
PUT /api/v2/state-versions/{id}/json-content
Accepted and discarded (placeholder for future use).
POST /api/v2/runs
Request body:
{
"data": {
"type": "runs",
"attributes": {
"message": "Triggered from API",
"is-destroy": false,
"auto-apply": false,
"plan-only": false
},
"relationships": {
"workspace": {
"data": {
"id": "ws-abc123",
"type": "workspaces"
}
}
}
}
}Required permission: plan for plan-only runs, write for apply runs.
Run objects include the following drift detection attributes in responses:
| Attribute | Type | Description |
|---|---|---|
is-drift-detection |
boolean | true if the run was created by the drift detection scheduler |
has-changes |
boolean or null | Whether the plan detected infrastructure changes. null if the plan has not completed yet |
Drift detection runs are always plan-only and are not counted in the workspace's normal run queue.
GET /api/v2/runs/{run_id}
GET /api/v2/workspaces/{id}/runs
POST /api/v2/runs/{run_id}/actions/confirm
Required permission: write on the workspace.
POST /api/v2/runs/{run_id}/actions/discard
Required permission: write on the workspace.
POST /api/v2/runs/{run_id}/actions/cancel
Required permission: write on the workspace.
POST /api/v2/runs/{run_id}/actions/retry
Creates a new run from a terminal run (applied, errored, canceled, discarded) using the same workspace, configuration version, VCS metadata, and settings. Returns a 409 if the run is not in a terminal state.
Required permission: plan on the workspace (or write for apply runs).
GET /api/v2/workspaces/{workspace_id}/runs/events
Server-Sent Events stream for real-time run status updates. The stream emits run_update events whenever a run in the workspace changes state. Used by the web UI for live updates without polling.
Required permission: read on the workspace.
GET /api/v2/runs/{run_id}/plan
Returns plan metadata and log download URL.
GET /api/v2/runs/{run_id}/apply
Returns apply metadata and log download URL.
Run triggers create cross-workspace dependency chains. When a source workspace completes an apply, all downstream workspaces with an inbound trigger automatically get a new run queued.
POST /api/v2/workspaces/{id}/run-triggers
Request body:
{
"data": {
"relationships": {
"sourceable": {
"data": {
"id": "ws-source-workspace-id",
"type": "workspaces"
}
}
}
}
}Required permission: admin on the destination workspace.
Validation:
- Source and destination must be different workspaces
- No duplicate triggers for the same pair
- Maximum 20 source workspaces per destination
Example:
curl -s \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-X POST \
https://terrapod.example.com/api/v2/workspaces/ws-abc123/run-triggers \
-d '{
"data": {
"relationships": {
"sourceable": {
"data": {"id": "ws-def456", "type": "workspaces"}
}
}
}
}'GET /api/v2/workspaces/{id}/run-triggers?filter[run-trigger][type]=inbound|outbound
inbound: triggers where this workspace is the destination (what triggers runs here?)outbound: triggers where this workspace is the source (what does my apply trigger?)
The filter[run-trigger][type] parameter is required (422 if missing).
Required permission: read on the workspace.
Example:
curl -s \
-H "Authorization: Bearer $TOKEN" \
"https://terrapod.example.com/api/v2/workspaces/ws-abc123/run-triggers?filter[run-trigger][type]=inbound"GET /api/v2/run-triggers/{id}
Required permission: read on the destination workspace.
DELETE /api/v2/run-triggers/{id}
Required permission: admin on the destination workspace.
Example:
curl -s \
-H "Authorization: Bearer $TOKEN" \
-X DELETE \
https://terrapod.example.com/api/v2/run-triggers/rt-abc123POST /api/v2/workspaces/{id}/configuration-versions
Request body:
{
"data": {
"type": "configuration-versions",
"attributes": {
"auto-queue-runs": true
}
}
}Response includes: upload-url attribute with a presigned URL for uploading the tarball.
Required permission: write on the workspace.
PUT <upload-url>
Content-Type: application/octet-stream
<tarball bytes>
No auth required (presigned URL).
GET /api/v2/workspaces/{id}/vars
Required permission: read on the workspace. Sensitive values are never returned.
POST /api/v2/workspaces/{id}/vars
Request body:
{
"data": {
"type": "vars",
"attributes": {
"key": "AWS_REGION",
"value": "eu-west-1",
"category": "env",
"sensitive": false,
"description": "AWS region for provider"
}
}
}category is either terraform (injected as TF_VAR_{key}) or env (injected as raw env var).
Required permission: write on the workspace.
PATCH /api/v2/workspaces/{id}/vars/{var_id}
DELETE /api/v2/workspaces/{id}/vars/{var_id}
Required permission: write on the workspace.
GET /api/v2/organizations/{org}/varsets
POST /api/v2/organizations/{org}/varsets
Required permission: Platform admin.
GET /api/v2/varsets/{varset_id}/relationships/vars
POST /api/v2/varsets/{varset_id}/relationships/vars
PATCH /api/v2/varsets/{varset_id}/relationships/vars/{var_id}
DELETE /api/v2/varsets/{varset_id}/relationships/vars/{var_id}
POST /api/v2/varsets/{varset_id}/relationships/workspaces
DELETE /api/v2/varsets/{varset_id}/relationships/workspaces
GET /api/v2/registry/modules/{namespace}/{name}/{provider}/versions
GET /api/v2/registry/modules/{namespace}/{name}/{provider}/{version}/download
GET /api/v2/organizations/{org}/registry-modules
POST /api/v2/organizations/{org}/registry-modules
GET /api/v2/organizations/{org}/registry-modules/private/default/{name}/{provider}
DELETE /api/v2/organizations/{org}/registry-modules/private/default/{name}/{provider}
POST /api/v2/organizations/{org}/registry-modules/private/default/{name}/{provider}/versions
DELETE /api/v2/organizations/{org}/registry-modules/private/default/{name}/{provider}/versions/{version}
PATCH /api/v2/organizations/{org}/registry-modules/private/default/{name}/{provider}
Required permission: admin on the module.
Self-lockout protection: If the request changes labels and the new labels would reduce the caller's own access level, the API returns 409 Conflict. Re-submit with "force": true in the attributes to confirm.
All module responses (show and list) include a permissions object:
{
"permissions": {
"can-update": true,
"can-destroy": true,
"can-create-version": true
}
}Create a version, then upload the tarball to the presigned URL returned in the response.
GET /api/v2/registry/providers/{namespace}/{type}/versions
GET /api/v2/registry/providers/{namespace}/{type}/{version}/download/{os}/{arch}
GET /api/v2/organizations/{org}/registry-providers
POST /api/v2/organizations/{org}/registry-providers
GET /api/v2/organizations/{org}/registry-providers/private/default/{name}
DELETE /api/v2/organizations/{org}/registry-providers/private/default/{name}
POST /api/v2/organizations/{org}/registry-providers/private/default/{name}/versions
GET /api/v2/organizations/{org}/registry-providers/private/default/{name}/versions
DELETE /api/v2/organizations/{org}/registry-providers/private/default/{name}/versions/{version}
POST /api/v2/organizations/{org}/registry-providers/private/default/{name}/versions/{version}/platforms
PATCH /api/v2/organizations/{org}/registry-providers/private/default/{name}
Required permission: admin on the provider.
Self-lockout protection: If the request changes labels and the new labels would reduce the caller's own access level, the API returns 409 Conflict. Re-submit with "force": true in the attributes to confirm.
All provider responses (show and list) include a permissions object:
{
"permissions": {
"can-update": true,
"can-destroy": true,
"can-create-version": true
}
}GET /api/registry/private/v2/gpg-keys
POST /api/registry/private/v2/gpg-keys
GET /api/registry/private/v2/gpg-keys/{namespace}/{key_id}
DELETE /api/registry/private/v2/gpg-keys/{namespace}/{key_id}
GET /api/v2/organizations/{org}/agent-pools
POST /api/v2/organizations/{org}/agent-pools
Request body:
{
"data": {
"type": "agent-pools",
"attributes": {
"name": "aws-prod",
"service-account-name": "terrapod-runner-aws"
}
}
}Required permission: Platform admin.
GET /api/v2/agent-pools/{id}
DELETE /api/v2/agent-pools/{id}
POST /api/v2/agent-pools/{id}/authentication-tokens
GET /api/v2/agent-pools/{id}/authentication-tokens
POST /api/v2/agent-pools/join
Registers a listener using a join token. The token identifies the pool — no pool ID needed in the URL. No Bearer auth required; the join token in the body IS the credential.
Request body:
{
"join_token": "<raw-token>",
"name": "my-listener",
"runner_definitions": ["standard"]
}Response: listener ID, pool ID, X.509 certificate, private key, CA certificate.
If a listener with the same name already exists, its certificate is reissued (handles pod restarts).
Legacy endpoint (still supported):
POST /api/v2/agent-pools/{pool_id}/listeners/join
POST /api/v2/listeners/{id}/heartbeat
POST /api/v2/listeners/{id}/renew
GET /api/v2/listeners/{id}/runs/next
Returns the next queued run for this listener.
PATCH /api/v2/listeners/{id}/runs/{run_id}
Reports run status changes (planning, planned, applying, applied, errored).
GET /api/v2/organizations/{org}/vcs-connections
POST /api/v2/organizations/{org}/vcs-connections
GitHub example:
{
"data": {
"type": "vcs-connections",
"attributes": {
"name": "my-github",
"provider": "github",
"github-app-id": 12345,
"github-installation-id": 112887490,
"github-account-login": "my-org",
"github-account-type": "Organization",
"private-key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
}
}
}GitLab example:
{
"data": {
"type": "vcs-connections",
"attributes": {
"name": "my-gitlab",
"provider": "gitlab",
"token": "glpat-xxxxxxxxxxxxxxxxxxxx"
}
}
}Required permission: Platform admin.
GET /api/v2/vcs-connections/{id}
DELETE /api/v2/vcs-connections/{id}
POST /api/v2/vcs-events/github
Validates HMAC-SHA256 signature and triggers an immediate poll cycle. The webhook secret must match TERRAPOD_VCS__GITHUB__WEBHOOK_SECRET.
GET /api/v2/roles
Returns built-in and custom roles.
Required permission: Platform admin or audit.
POST /api/v2/roles
Request body:
{
"data": {
"type": "roles",
"attributes": {
"name": "developer",
"description": "Development workspace access",
"workspace-permission": "write",
"allow-labels": {"env": "dev"},
"allow-names": [],
"deny-labels": {},
"deny-names": []
}
}
}Required permission: Platform admin.
GET /api/v2/roles/{name}
PATCH /api/v2/roles/{name}
DELETE /api/v2/roles/{name}
Built-in roles cannot be deleted.
GET /api/v2/role-assignments
Required permission: Platform admin or audit.
PUT /api/v2/role-assignments
Request body:
{
"data": {
"type": "role-assignments",
"attributes": {
"provider-name": "local",
"email": "alice@example.com",
"roles": ["developer", "sre-reader"]
}
}
}Required permission: Platform admin.
DELETE /api/v2/role-assignments/{provider}/{email}/{role}
POST /api/v2/users/{user_id}/authentication-tokens
GET /api/v2/users/{user_id}/authentication-tokens
GET /api/v2/authentication-tokens/{id}
DELETE /api/v2/authentication-tokens/{id}
GET /api/v2/binary-cache/{tool}/{version}/{os}/{arch}
Returns a 302 redirect to a presigned URL for the binary. tool is terraform or tofu.
GET /api/v2/admin/binary-cache
POST /api/v2/admin/binary-cache/warm
Pre-cache a specific tool version.
DELETE /api/v2/admin/binary-cache/{tool}/{version}
GET /api/v2/admin/health-dashboard
Returns platform-wide health data including workspace status summaries, recent run statistics, and listener availability. Intended for admin dashboards and monitoring integrations.
Required permission: Platform admin or audit.
Response:
{
"data": {
"id": "health-dashboard",
"type": "health-dashboards",
"attributes": {
"workspaces": {
"total": 42,
"locked": 2,
"drift-enabled": 15,
"by-drift-status": {
"unchecked": 5,
"no-drift": 30,
"drifted": 5,
"errored": 2
},
"stale": [{"id": "ws-uuid", "name": "prod-infra", "last-applied-at": "...", "days-since-apply": 18, "drift-status": "drifted"}]
},
"runs": {
"queued": 4,
"in-progress": 2,
"recent-24h": {
"total": 18,
"applied": 12,
"errored": 3,
"canceled": 1
},
"average-plan-duration-seconds": 45,
"average-apply-duration-seconds": 120
},
"listeners": {
"total": 3,
"online": 2,
"offline": 1,
"details": [{"id": "listener-uuid", "name": "pool-listener-pod", "pool-name": "default", "status": "online", "capacity": 5, "active-runs": 1, "last-heartbeat": "..."}]
}
}
}
}| Attribute | Description |
|---|---|
workspaces.total |
Total number of workspaces |
workspaces.locked |
Workspaces currently locked |
workspaces.drift-enabled |
Number of workspaces with drift detection enabled |
workspaces.by-drift-status |
Breakdown of workspaces by drift status |
workspaces.stale |
Top 20 workspaces by staleness (never-applied first) |
runs.queued |
Runs currently in queued state |
runs.in-progress |
Runs currently planning or applying |
runs.recent-24h |
24-hour breakdown (total, applied, errored, canceled) |
runs.average-plan-duration-seconds |
Average plan duration (last 24h) |
runs.average-apply-duration-seconds |
Average apply duration (last 24h) |
listeners.total |
Total registered listeners |
listeners.online |
Listeners with a recent heartbeat (within 180s) |
listeners.offline |
Listeners with no recent heartbeat |
listeners.details |
Per-listener status, pool, capacity, and active runs |
GET /v1/providers/{hostname}/{namespace}/{type}/index.json
GET /v1/providers/{hostname}/{namespace}/{type}/{version}.json
Returns platform-specific download URLs with zh: (zip hash) checksums.
GET /api/v2/auth/providers
GET /api/v2/auth/authorize
GET /api/v2/auth/callback
POST /api/v2/auth/callback
GET /api/v2/auth/sessions
POST /api/v2/auth/logout
GET /oauth/authorize
POST /oauth/token
Immutable record of API requests. Requires admin or audit role.
GET /api/v2/admin/audit-log
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
filter[actor] |
string | Filter by actor email |
filter[resource-type] |
string | Filter by resource type (e.g. workspaces, runs) |
filter[action] |
string | Filter by HTTP method (GET, POST, PATCH, DELETE) |
filter[since] |
datetime | Only entries after this timestamp (RFC3339) |
filter[until] |
datetime | Only entries before this timestamp (RFC3339) |
page[number] |
integer | Page number (default: 1) |
page[size] |
integer | Page size (default: 20, max: 100) |
Response: JSON:API list of audit-log-entries with pagination metadata.
Example:
curl "https://terrapod.example.com/api/v2/admin/audit-log?filter[actor]=admin@example.com&page[size]=10" \
-H "Authorization: Bearer $TERRAPOD_TOKEN"User management endpoints. List and show require admin or audit role. Update and delete require admin role.
GET /api/v2/organizations/{org}/users
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
filter[email] |
string | Filter by email (case-insensitive substring match) |
page[number] |
integer | Page number (default: 1) |
page[size] |
integer | Page size (default: 20, max: 100) |
GET /api/v2/users/{email}
PATCH /api/v2/users/{email}
Updatable attributes: is-active, display-name.
When is-active is set to false, all sessions for that user are revoked immediately.
DELETE /api/v2/users/{email}
Cascades: revokes all sessions, deletes all role assignments.
Workspace-scoped notifications that fire on run lifecycle events. Three destination types: generic (webhook with HMAC-SHA512 signing), slack (Block Kit formatted), and email (SMTP).
POST /api/v2/workspaces/{id}/notification-configurations
Request body:
{
"data": {
"type": "notification-configurations",
"attributes": {
"name": "deploy-alerts",
"destination-type": "generic",
"url": "https://example.com/webhook",
"token": "my-hmac-secret",
"enabled": true,
"triggers": ["run:completed", "run:errored"]
}
}
}Destination types:
| Type | Required fields | Optional |
|---|---|---|
generic |
url |
token (HMAC-SHA512 signing) |
slack |
url (Slack webhook URL) |
— |
email |
email-addresses (list) |
— |
Valid triggers: run:created, run:planning, run:needs_attention, run:planned, run:applying, run:completed, run:errored, run:drift_detected
Required permission: admin on the workspace.
GET /api/v2/workspaces/{id}/notification-configurations
Required permission: read on the workspace.
GET /api/v2/notification-configurations/{id}
Required permission: read on the associated workspace.
PATCH /api/v2/notification-configurations/{id}
Same body format as create. Only include attributes to change. Token is never returned in responses — only has-token: true/false.
Required permission: admin on the workspace.
DELETE /api/v2/notification-configurations/{id}
Required permission: admin on the workspace.
POST /api/v2/notification-configurations/{id}/actions/verify
Sends a test payload to the configured destination and returns the delivery response.
Required permission: admin on the workspace.
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 204 | Deleted (no content) |
| 302 | Redirect (presigned URLs, OAuth flows) |
| 400 | Bad request (validation error) |
| 401 | Unauthorized (missing or invalid token) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not found |
| 409 | Conflict (lock conflict, duplicate resource, label change would reduce your access) |
| 422 | Unprocessable entity (semantic validation error) |
| 503 | Service unavailable (readiness check failed) |
