Skip to main content
GET /owners/:ownerId/query Returns a paginated, sortable, filterable view of an owner’s governance tree — each node with its hierarchy position, budget configuration, and current usage. Use this endpoint to power dashboards, admin UIs, and leaderboards.
Usage figures are served from a periodically-refreshed read model and may lag the live counter by a few minutes. This endpoint never gates — use Check for real-time gate decisions.

Setup

QueryApi is exported from @stigg/governance-client but is not included in the createGovernanceClient factory. Instantiate it separately using the same axios instance that carries your auth headers.
import axios from 'axios';
import { QueryApi, Configuration } from '@stigg/governance-client';

const axiosInstance = axios.create({ maxRedirects: 0 });
axiosInstance.interceptors.request.use(config => {
  config.headers.set('Authorization',          `Bearer ${process.env.GOVERNANCE_ACCESS_TOKEN}`);
  config.headers.set('x-stigg-account-id',     process.env.STIGG_ACCOUNT_ID);
  config.headers.set('x-stigg-environment-id', process.env.STIGG_ENVIRONMENT_ID);
  return config;
});

const queryApi = new QueryApi(
  new Configuration({ basePath: process.env.GOVERNANCE_API_URL }),
  process.env.GOVERNANCE_API_URL,
  axiosInstance,
);

Basic query

const { data } = await queryApi.queryControllerList('cus-acme');
// data.data: QueryResponse[]
// data.pagination: { next: string | null, prev: string | null }

Response shape

{
  "data": [
    {
      "entityId": "team-eng",
      "parentId": "org-acme",
      "entityType": "team",
      "capabilityId": "ai-tokens",
      "scopeEntityIds": [],
      "usageLimit": 200000,
      "currentUsage": 164000,
      "utilization": 0.82,
      "cadence": "P1M",
      "usagePeriodStart": "2026-05-01T00:00:00.000Z",
      "usagePeriodEnd": "2026-06-01T00:00:00.000Z"
    },
    {
      "entityId": "team-eng",
      "parentId": "org-acme",
      "entityType": "team",
      "capabilityId": "ai-tokens",
      "scopeEntityIds": ["model-gpt4o"],
      "usageLimit": 10000,
      "currentUsage": 2600,
      "utilization": 0.26,
      "cadence": "P1M",
      "usagePeriodStart": "2026-05-01T00:00:00.000Z",
      "usagePeriodEnd": "2026-06-01T00:00:00.000Z"
    }
  ],
  "pagination": {
    "next": "eyJpZCI6InRlYW0tZW5nIn0=",
    "prev": null
  }
}
FieldDescription
entityIdExternal ID of the entity (hierarchy node).
parentIdExternal ID of the parent entity; null for a root node. Use this to rebuild the tree client-side.
entityTypeExternal ID of the entity type (e.g., org, team, user).
capabilityIdExternal ID of the capability this budget is for.
scopeEntityIdsThe cardinality scope. [] is the node-wide budget; a non-empty set is a dimension-scoped sub-budget.
usageLimitHard usage limit per cadence period.
currentUsageUsage consumed in the current cadence period (may lag by minutes).
utilizationcurrentUsage / usageLimit. 1.0 when at or over limit; null if no limit.
cadenceISO-8601 reset cadence (e.g., P1M).
usagePeriodStartStart of the cadence period the snapshot belongs to.
usagePeriodEndWhen usage resets (exclusive).

Filtering

Filter by capability

const { data } = await queryApi.queryControllerList(
  'cus-acme',
  ['ai-tokens', 'api-calls'], // capabilityIds
);

Filter by entity type

const { data } = await queryApi.queryControllerList(
  'cus-acme',
  undefined,         // capabilityIds
  undefined,         // scope
  ['team', 'user'],  // entityTypeIds
);

Filter by scope

scope valueRows included
'all' (default)All rows
'nodeWide'Only node-wide budgets (scopeEntityIds: [])
'scoped'Only dimension-scoped sub-budgets
const { data } = await queryApi.queryControllerList(
  'cus-acme',
  undefined,    // capabilityIds
  'nodeWide',   // scope
);

Filter by utilization

// Entities at ≥ 80% utilization
const { data: nearLimit } = await queryApi.queryControllerList(
  'cus-acme',
  undefined, undefined, undefined, undefined,
  0.8, // minUtilization
);

// Entities at or over their limit
const { data: overLimit } = await queryApi.queryControllerList(
  'cus-acme',
  undefined, undefined, undefined, undefined,
  1.0, // minUtilization
);

Search by entity ID

Case-insensitive substring match on the entity ID.
const { data } = await queryApi.queryControllerList(
  'cus-acme',
  undefined, undefined, undefined,
  'team', // entityIdSearch
);

Sorting

sortBy valueDescription
'utilization' (default)currentUsage / usageLimit — cross-capability safe
'currentUsage'Raw usage count
'usageLimit'Configured limit
'scopeSize'Node-wide rows first, then scoped by set size
'id'Entity ID alphabetical
'createdAt'Creation timestamp
// Teams closest to their limit, descending
const { data } = await queryApi.queryControllerList(
  'cus-acme',
  ['ai-tokens'],    // capabilityIds
  undefined,        // scope
  ['team'],         // entityTypeIds
  undefined,        // entityIdSearch
  undefined,        // minUtilization
  undefined,        // after
  20,               // limit
  'utilization',    // sortBy
  'desc',           // order
);

Pagination

The query endpoint uses forward-only cursor pagination.
async function* queryAllNodes(ownerId: string) {
  let after: string | undefined;

  do {
    const { data } = await queryApi.queryControllerList(
      ownerId,
      undefined, undefined, undefined, undefined, undefined,
      after, // after cursor
      50,    // limit
    );
    yield* data.data;
    after = data.pagination.next ?? undefined;
  } while (after);
}
pagination.next is null when you have reached the last page.

queryControllerList parameter reference

queryControllerList(
  ownerId:        string,                       // required
  capabilityIds?: string[],                     // filter by capability
  scope?:         'all' | 'nodeWide' | 'scoped', // default 'all'
  entityTypeIds?: string[],                     // filter by entity type
  entityIdSearch?: string,                      // substring match on entity ID
  minUtilization?: number,                      // minimum utilization ratio
  after?:         string,                       // forward cursor
  limit?:         number,                       // 1–100, default 20
  sortBy?:        'utilization' | 'currentUsage' | 'usageLimit' | 'scopeSize' | 'id' | 'createdAt',
  order?:         'asc' | 'desc',               // default 'desc'
)

Rebuilding the tree client-side

Each row carries parentId, so you can reconstruct the full hierarchy from a flat response:
type TreeNode = QueryResponse & { children: TreeNode[] };

function buildTree(rows: QueryResponse[]): TreeNode[] {
  const byId = new Map<string, TreeNode>(
    rows.map(r => [r.entityId, { ...r, children: [] }])
  );
  const roots: TreeNode[] = [];

  for (const node of byId.values()) {
    if (node.parentId && byId.has(node.parentId)) {
      byId.get(node.parentId)!.children.push(node);
    } else {
      roots.push(node);
    }
  }

  return roots;
}