API Reference

Stategraph provides a REST API for programmatic access to states, queries, and management operations.

Authentication

Stategraph supports two authentication methods:

Bearer Token (API Key)

For CLI, Terraform, and programmatic access, use Bearer authentication with API keys:

curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" http://localhost:8080/api/v1/...

API keys are created via the Settings page in the UI.

For browser-based access, authentication uses session cookies set after login (local authentication or OAuth). This is handled automatically by the browser.

Authentication Modes

Stategraph supports three authentication modes:
- Local Authentication - Email/password with user management (default)
- Google OAuth - SSO via Google Workspace
- Generic OIDC - Any OIDC-compatible provider

Base URL

http://localhost:8080/api/v1

Replace localhost:8080 with your Stategraph server address if different.


User Endpoints

Get Current User

GET /api/v1/whoami

Returns information about the authenticated user.

Response

{
  "id": "f30ed1f9-be44-46a3-9050-03e9561e94f0",
  "name": "User Name",
  "email": "user@example.com",
  "is_admin": false,
  "type": "user"
}
Field Type Description
id string Unique user ID (UUID)
name string Display name
email string User email address
is_admin boolean Whether user has admin privileges
type enum user, api, or system

List User Tenants

GET /api/v1/user/tenants

Returns tenants the user has access to.

Response

{
  "results": [
    {
      "id": "tenant-123",
      "name": "My Organization"
    }
  ]
}

Authentication Endpoints

Get Login Options

GET /api/v1/login/options

Returns available authentication methods.

Response

When local authentication is the only method configured:

{
  "options": []
}

When OAuth is configured:

{
  "options": [
    {
      "type": "google",
      "display_name": "Sign in with Google"
    }
  ]
}

Login with Password

POST /api/v1/login/password

Authenticates user with email and password (local authentication only).

Request Body

{
  "email": "user@example.com",
  "password": "your-password"
}

Response

{
  "session_token": "eyJhbGciOiJIUzI1NiIs...",
  "success": true
}

The session_token can be used for Bearer authentication. A session cookie is also set for browser-based access.

Check Setup Status

GET /api/v1/setup/status

Returns whether initial setup is needed (no users exist).

Response

{
  "needs_setup": false
}

Create First Admin User

POST /api/v1/setup/admin

Creates the first admin user during initial setup. Only works when no users exist.

Request Body

{
  "email": "admin@example.com",
  "password": "secure-password",
  "name": "Admin User",
  "organization": "My Organization"
}

Response

{
  "session_token": "eyJhbGciOiJIUzI1NiIs...",
  "user_id": "f30ed1f9-be44-46a3-9050-03e9561e94f0"
}

The session_token can be used for Bearer authentication.

Complete Setup

POST /api/v1/setup/complete

Marks the initial setup as complete. Called after creating the first admin user.

Response: 200 OK

Logout

GET /api/v1/logout

Clears session cookie and logs out the user.

Response: 200 OK with redirect


User Management Endpoints

All user management endpoints require admin authentication.

List Users

GET /api/v1/users

Returns list of users with cursor-based pagination and filtering.

Parameters

Parameter Type Required Description
type query No Filter by user type: user or api
limit query No Max results (default: 25)
cursor query No Pagination cursor from previous response
search query No Search by name or email

Response

{
  "users": [
    {
      "id": "f30ed1f9-be44-46a3-9050-03e9561e94f0",
      "name": "John Doe",
      "email": "john@example.com",
      "is_admin": false,
      "type": "user",
      "auth_origin": "local",
      "created_at": "2024-01-15T10:30:00Z"
    }
  ],
  "total_count": 10,
  "limit": 25,
  "has_more": false,
  "next_cursor": null
}
Field Type Description
users array List of user objects
total_count integer Total number of matching users
limit integer Limit used for this request
has_more boolean Whether more results are available
next_cursor string Cursor for next page (null if no more)
auth_origin string Authentication origin (e.g., local, google, oidc)

Get User Details

GET /api/v1/users/detail?user_id={user_id}

Returns detailed information about a specific user.

Response

{
  "id": "f30ed1f9-be44-46a3-9050-03e9561e94f0",
  "name": "John Doe",
  "email": "john@example.com",
  "is_admin": false,
  "type": "user",
  "created_at": "2024-01-15T10:30:00Z"
}

Note: The response also includes auth_origin (how the user authenticates) and avatar_url when those values are set.

Create User

POST /api/v1/users

Creates a new user account.

Request Body

{
  "name": "Jane Doe",
  "email": "jane@example.com",
  "password": "secure-password",
  "is_admin": false
}
Field Type Required Description
name string Yes Display name
email string Yes Email address (used for login)
password string Yes Password (8-128 characters)
is_admin boolean No Grant admin privileges (default: false)

Response

{
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "is_admin": false,
  "type": "user"
}

Update User

PUT /api/v1/users/update?user_id={user_id}

Updates user information.

Request Body

{
  "name": "Jane Smith",
  "email": "jane.smith@example.com"
}

Response: 200 OK with updated user object

Delete User

DELETE /api/v1/users/delete?user_id={user_id}

Soft-deletes a user (marks as deleted, invalidates API keys).

Response: 204 No Content

Change User Password

POST /api/v1/users/change-password?user_id={user_id}

Changes a user's password.

Request Body

{
  "new_password": "new-secure-password",
  "current_password": "old-password"
}
Field Type Required Description
new_password string Yes New password (8-128 characters)
current_password string Conditional Required when changing own password

Response: 200 OK

Note: Admins can change any user's password without providing current_password. Users changing their own password must provide current_password.

Toggle Admin Status

POST /api/v1/users/toggle-admin?user_id={user_id}

Grants or revokes admin privileges.

Request Body

{
  "is_admin": true
}

Response: 200 OK with updated user object

Note: Cannot remove admin privileges from the last admin user.

Create Service Account

POST /api/v1/api-users

Creates a service account (API user) for programmatic access.

Request Body

{
  "name": "ci-production",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000"
}

Response

{
  "user_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

Important: The token is only returned once. Save it immediately.


State Endpoints

List States

GET /api/v1/tenants/{tenant_id}/states

Returns all states for a tenant.

Parameters

Parameter Type Required Description
tenant_id path Yes Tenant ID
page query No Pagination cursor
limit query No Max results (default: 100)
q query No SQL filter query

Response

{
  "results": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "networking",
      "group_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "workspace": "default",
      "created_at": "2024-01-15T10:30:00Z"
    }
  ]
}

Create State

POST /api/v1/tenants/{tenant_id}/states

Creates a new state.

Request Body

{
  "name": "networking",
  "group_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "workspace": "production"
}
Field Type Required Description
name string Yes State name
group_id uuid No Group identifier (UUID; defaults to a generated UUID)
workspace string No Workspace name (default: default)

Response: 201 Created with state object

Import State

POST /api/v1/tenants/{tenant_id}/states/import

Imports an existing Terraform state file.

Request Body

{
  "name": "networking",
  "group_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "workspace": "production",
  "state": { ... },
  "tags": { "source": "migration" }
}
Field Type Required Description
name string Yes State name
state object Yes Terraform state JSON
group_id uuid No Group identifier (UUID; defaults to a generated UUID)
workspace string No Workspace name
tags object No Metadata tags

Export State

GET /api/v1/states/{state_id}/export

Returns the full Terraform state as JSON (equivalent to terraform state pull / stategraph states export).

curl "http://localhost:8080/api/v1/states/$STATE_ID/export" \
  -H "Authorization: Bearer $STATEGRAPH_API_KEY"

Delete State

DELETE /api/v1/states/{state_id}

Permanently deletes a state and all of its related data (resources, instances, providers, outputs,
raw states, transaction logs, and check entries/results). This cannot be undone. Requires admin
privileges.

Parameters

Parameter Type Required Description
state_id path Yes State ID (UUID)

Authorization

Requires admin privileges.

Response Codes

Code Description
200 OK State successfully deleted
401 Unauthorized Not authenticated
403 Forbidden User is not an admin
404 Not Found State does not exist
500 Internal Server Error Server error occurred

Example

curl -X DELETE "http://localhost:8080/api/v1/states/550e8400-e29b-41d4-a716-446655440000" \
  -H "Authorization: Bearer $STATEGRAPH_API_KEY"

Error Response (403 Forbidden)

{
  "id": "ADMIN_REQUIRED",
  "data": "Admin privileges required"
}

Error Response (404 Not Found)

{
  "id": "STATE_NOT_FOUND",
  "data": "State not found"
}

Instance Endpoints

List Instances

GET /api/v1/states/{state_id}/instances

Returns resource instances for a state.

Parameters

Parameter Type Required Description
state_id path Yes State ID
page query No Pagination cursor
limit query No Max results
q query No SQL filter

Response

{
  "results": [
    {
      "address": "aws_instance.web",
      "type": "aws_instance",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "module": null,
      "attributes": { ... },
      "dependencies": ["aws_subnet.main", "aws_security_group.web"]
    }
  ]
}

Get Blast Radius

GET /api/v1/states/{state_id}/instances/{instance_address}/blast-radius

Returns resources affected by changes to the specified instance.

Parameters

Parameter Type Required Description
state_id path Yes State ID
instance_address path Yes URL-encoded instance address

Response

{
  "results": [
    {
      "address": "aws_eip.web",
      "resource_address": "aws_eip.web",
      "distance": 1
    }
  ]
}

Module Endpoints

List Modules

GET /api/v1/states/{state_id}/modules

Returns modules in a state.

Response

{
  "results": [
    {
      "name": "module.vpc",
      "instance_count": 25,
      "resource_count": 10
    }
  ]
}

Summary Endpoints

State Summary

GET /api/v1/states/{state_id}/summary

Returns aggregate statistics for a state.

Response

{
  "instances": 150,
  "resources": 45,
  "modules": 8,
  "providers": 3,
  "edges": 234
}

Resources Summary

GET /api/v1/states/{state_id}/resources/summary

Returns instance counts by resource type.

Response

{
  "aws_instance": { "instances": 20 },
  "aws_security_group": { "instances": 15 },
  "aws_subnet": { "instances": 6 }
}

Tenant Summary

GET /api/v1/tenants/{tenant_id}/summary[?state_id={state_id}]

Returns inventory aggregates across a tenant (or a single state with ?state_id): totals, provider
and resource-type distributions, graph structure, largest/most-deployed modules, and orphaned-resource
counts — the data behind the overview dashboard.

Response (abridged)

{
  "total_states": 15,
  "total_resources": 1234,
  "total_instances": 1890,
  "total_modules": 42,
  "total_providers": 3,
  "total_edges": 2310,
  "provider_distribution": [],
  "resource_type_distribution": [],
  "top_resource_type": "aws_iam_role",
  "largest_module": "module.vpc",
  "most_deployed_module": "module.service",
  "orphaned_count": 7,
  "graph_roots": 12,
  "graph_leaves": 340
}

Query Endpoints

Execute SQL Query

GET /api/v1/mql

Executes an SQL query across all states.

Parameters

Parameter Type Required Description
q query Yes SQL query string
page query No Pagination cursor
tz query No Timezone for date formatting

Example

curl "http://localhost:8080/api/v1/mql?q=SELECT%20*%20FROM%20resources%20WHERE%20type%20%3D%20%27aws_instance%27" \
  -H "Authorization: Bearer $STATEGRAPH_API_KEY"

Response

[
  {
    "address": "aws_instance.web",
    "type": "aws_instance",
    ...
  }
]

Result size

The number of rows returned is controlled by the query's own LIMIT clause, capped at a maximum of 1000 rows. A limit query parameter is not supported and is ignored; set the bound with LIMIT inside the query instead.

Cursor pagination

Cursor pagination is exposed through an RFC 5988 Link response header, and is only available when the query includes an ORDER BY clause (a stable sort order is required to page deterministically).

  • With ORDER BY: when more rows remain, the response includes a Link header with rel="next" (and rel="prev" when paging through results). Follow the Link URL to fetch the next page.
Link: <http://localhost:8080/api/v1/mql?page=...&q=...>; rel="next"
  • Without ORDER BY: no Link header is emitted. Instead the response carries an mql-pagination-error: ORDER_BY_MISSING header, signalling that the result cannot be paginated. Add an ORDER BY clause to enable paging.

Get SQL Schema

GET /api/v1/mql/schema

Returns the SQL schema for autocomplete.


Transaction Endpoints

List Transactions

GET /api/v1/tenants/{tenant_id}/tx

Returns transactions for a tenant.

Response

{
  "results": [
    {
      "id": "455fe705-f27f-4335-9355-dbe8f14098df",
      "created_at": "2024-01-15T10:30:00Z",
      "created_by": "f30ed1f9-be44-46a3-9050-03e9561e94f0",
      "completed_at": "2024-01-15T10:30:05Z",
      "completed_by": "f30ed1f9-be44-46a3-9050-03e9561e94f0",
      "state": "committed",
      "tags": {"desc": "Backend update"}
    }
  ]
}

Create Transaction

POST /api/v1/tenants/{tenant_id}/tx/create

Creates a new transaction.

Request Body

{
  "tags": {
    "pipeline": "github-actions",
    "commit": "abc123"
  }
}

Get Transaction Logs

GET /api/v1/tx/{tx_id}/logs

Returns logs for a transaction.

Response

{
  "results": [
    {
      "id": "log-123",
      "action": "state_set",
      "object_type": "instance",
      "created_at": "2024-01-15T10:30:00Z",
      "state_id": "state-123",
      "data": { ... }
    }
  ]
}

Abort Transaction

POST /api/v1/tx/{tx_id}/abort

Aborts an active transaction.


API Key Endpoints

API keys (access tokens) authenticate the CLI, Terraform, and API requests as a Bearer token.

Create API Key

POST /api/v1/user/access-tokens

Request Body

{ "name": "my-api-key" }

Response — the token is returned once; store it securely.

{ "token": "eyJhbGciOiJIUzI1NiIs..." }

List API Keys

GET /api/v1/user/access-tokens

Response

{
  "tokens": [
    {
      "id": "...",
      "name": "my-api-key",
      "created_at": "2026-06-04T10:30:00Z",
      "owner_id": "...",
      "owner_name": "jane@example.com",
      "owner_type": "user"
    }
  ]
}

Each token object always includes id, name, created_at, owner_id, owner_name, and owner_type. The expiration field is only present when an expiry has been set on the token; it is omitted for tokens that never expire.

Revoke API Key

POST /api/v1/user/access-tokens/revoke?token_id={id}

Revokes the token with the given ID.

Response

{
  "id": "f02791c8-aa63-4cdf-acae-a7c968d8a831",
  "revoked": true
}

For Terraform CI/CD, a per-run session token (used as TF_HTTP_PASSWORD) is minted separately
via POST /api/v1/tx/session/create — see Transactions.


Gap Analysis Endpoints

Get Gap Analysis Config

GET /api/v1/tenants/{tenant_id}/gaps/config

Returns gap analysis configuration status.

Parameters

Parameter Type Required Description
provider query Yes Cloud provider (e.g., aws)

Response

{
  "status": "ready",
  "ready_for_gap_analysis": true,
  "ready_for_terraform_import": true,
  "has_aggregator": true,
  "aggregator_region": "us-east-1",
  "indexed_regions": ["us-east-1", "us-west-2"],
  "index_count": 2,
  "warnings": []
}

Run Gap Analysis

GET /api/v1/tenants/{tenant_id}/gaps

Returns unmanaged resources. Gap analysis runs asynchronously: the first request for a provider starts a background scan and returns { "status": "running", "started_at": <ts> }. Repeat the request to retrieve the result once the scan completes (subsequent calls are served from cache).

Parameters

Parameter Type Required Description
provider query Yes Cloud provider
source query No cache (default) or no-cache

Response

{
  "summary": {
    "total_aws_resources": 1500,
    "managed_by_stategraph": 1200,
    "unmanaged": 300,
    "phantom_filtered": 8
  },
  "unmanaged_resources": [ ... ],
  "fetched_at": 1705312800
}

Generate Import

POST /api/v1/tenants/{tenant_id}/gaps/import

Generates Terraform import blocks.

Request Body

{
  "provider": "aws",
  "resources": [
    {
      "arn": "arn:aws:s3:::bucket-name",
      "service": "s3",
      "resource_type": "s3:bucket",
      "region": "us-east-1",
      "owning_account_id": "123456789012"
    }
  ]
}

Response

{
  "import_blocks": "import { ... }",
  "provider_hcl": "provider \"aws\" { ... }",
  "generated_hcl": "resource \"aws_s3_bucket\" { ... }",
  "supported_count": 1,
  "unsupported_count": 0,
  "unsupported_resources": []
}

Version

GET /api/v1/version

Returns the server version.

Response

{ "version": "1.2.3" }

Cost Endpoints

Estimate and read infrastructure costs. See Cost Analysis for the full guide,
response shapes, and SQL access. Monetary fields are strings (parse as decimals). Cost analysis
requires the server to be configured with a pricing service.

Method Path Description
GET /api/v1/states/{state_id}/costs Latest cost snapshot for a state with per-resource breakdown (404 if never priced)
POST /api/v1/states/{state_id}/costs/calculate Trigger a recompute; returns 202 with a task (poll /api/v1/tasks/{id}); 503 if no pricing service
GET /api/v1/states/{state_id}/costs/unsupported Resources that did not contribute to the totals (coverage gaps)
GET /api/v1/tenants/{tenant_id}/costs Current cost rollup across all states (?tag_key to break down by a tag)
GET /api/v1/tenants/{tenant_id}/costs/history Cost over time, one point per day (?from, ?to, ?group_by, ?tag_key)
GET /api/v1/tenants/{tenant_id}/costs/tag-keys Tag keys available for grouping
GET /api/v1/tx/{tx_id}/costs Plan-time cost delta for a pending transaction (see Plan-time cost)

Cost history parameters

The from and to query parameters require a full ISO 8601 / RFC 3339 timestamp (for example 2026-06-01T00:00:00Z). A bare calendar date such as 2026-06-01 is rejected with 400 Bad Request and body {"id": "INVALID_DATE_PARAM", "data": "..."}.

group_by accepts provider, type, or tag. When group_by=tag, a tag_key query parameter is required; omitting it (or passing an unknown group_by value) returns 400 Bad Request with body {"id": "INVALID_GROUP_BY", "data": "..."}.

Plan-time cost

GET /api/v1/tx/{tx_id}/costs

Returns the current-vs-planned cost delta for a transaction opened by stategraph tf plan / tf mtx. See Plan-Time Cost for the CLI and the full response shape.

  • 200 — a tx-cost-delta: totals (each metric as current_* / planned_* / delta_*), a per-state states[] list with per-resource resources[] (each carrying a change_kind of added / removed / changed), and by_provider / by_type / by_tag delta breakdowns.
  • 202{"status": "computing"}: the preview isn't ready yet, or the transaction wasn't opened through stategraph planning. Retry shortly.
  • 404 — the transaction was aborted (which deletes its planned cost).
  • 401 — the caller isn't a member of the transaction's tenant.

Billing Source Endpoints

Admin-only endpoints for connecting cloud billing (FOCUS) exports — your actual cloud spend. See Cloud Billing (FOCUS). A non-admin caller receives 403.

Method Path Description
GET /api/v1/tenants/{tenant_id}/billing-sources List billing sources
POST /api/v1/tenants/{tenant_id}/billing-sources Create a source — body {provider, source_uri, region?, window_months?, enabled?}
PUT /api/v1/tenants/{tenant_id}/billing-sources/{id} Update a source (omitted fields unchanged)
POST /api/v1/tenants/{tenant_id}/billing-sources/{id}/sync Trigger a sync — body {window_start?}
DELETE /api/v1/tenants/{tenant_id}/billing-sources/{id} Delete a source and its loaded billing rows

OpenAPI Schema

Get OpenAPI Schema

GET /api/v1/openapi

Returns the full OpenAPI schema for the Stategraph API as JSON. This endpoint is unauthenticated and can be used to discover available endpoints, request/response types, and integrate with tools that consume OpenAPI specifications.

Example

curl http://localhost:8080/api/v1/openapi

Response: 200 OK with the complete OpenAPI JSON schema.

The schema is also available as a downloadable file from each release.


Error Responses

400 Bad Request

The response body for a 400 is endpoint-specific. Many endpoints return a structured error of the form:

{
  "id": "INVALID_DATE_PARAM",
  "data": "Error description"
}

where id is a machine-readable error code (for example INVALID_DATE_PARAM or INVALID_GROUP_BY). Some validation failures — such as rejecting a malformed UUID when creating a state — instead return a 400 with an empty body. Do not assume a JSON body is always present on a 400; check the status code first.

401 Unauthorized

No body. Authentication required.

404 Not Found

Resource not found.

500 Internal Server Error

Server error occurred.


Pagination

List endpoints support cursor-based pagination:

# First page
curl "http://localhost:8080/api/v1/tenants/$TENANT_ID/states?limit=50" \
  -H "Authorization: Bearer $STATEGRAPH_API_KEY"

# Next page (use cursor from previous response)
curl "http://localhost:8080/api/v1/tenants/$TENANT_ID/states?limit=50&page=cursor..." \
  -H "Authorization: Bearer $STATEGRAPH_API_KEY"