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.
Session Cookie
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 aLinkheader withrel="next"(andrel="prev"when paging through results). Follow theLinkURL to fetch the next page.
Link: <http://localhost:8080/api/v1/mql?page=...&q=...>; rel="next"
- Without
ORDER BY: noLinkheader is emitted. Instead the response carries anmql-pagination-error: ORDER_BY_MISSINGheader, signalling that the result cannot be paginated. Add anORDER BYclause 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
viaPOST /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— atx-cost-delta:totals(each metric ascurrent_*/planned_*/delta_*), a per-statestates[]list with per-resourceresources[](each carrying achange_kindofadded/removed/changed), andby_provider/by_type/by_tagdelta breakdowns.202—{"status": "computing"}: the preview isn't ready yet, or the transaction wasn't opened throughstategraphplanning. 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"