Skip to main content
Governance exposes two runtime operations: Check and Ingest.
  • Check reads the current usage and returns a gate decision. It never modifies state.
  • Ingest records a consumption event and increments the usage counter. It never gates.
The typical pattern is: check → allow or deny based on hasAccess → ingest (only if allowed).
Governance is opt-in. If an entity has no assignment for the requested capability, Check returns hasAccess: true with an empty checks array. There is no default deny.
Both Check and Ingest are fail-closed. If the governance cache is unavailable, they return 503 Service Unavailable rather than silently allowing or skipping. Plan for this in your error handling — a 503 from governance should be treated the same as any upstream dependency outage.
If you haven’t set up the client yet, see Setting up governance.

Check

POST /owners/:ownerId/check Returns whether the requested amount of a capability is permitted, along with current usage and the applicable limit. Call this before allowing consumption.

By entity IDs

Identify the governed entity by passing its IDs directly.
const { data: report } = await governance.check.checkControllerCheck('cus-acme', {
  entityIds:       ['team-eng'],
  capabilityId:    'ai-tokens',
  requestedAmount: 1000,
});

if (!report.hasAccess) {
  throw new Error('Usage limit reached');
}

By dimensions

If you don’t know the entity IDs at call time, pass the event dimensions and let the governance engine resolve the entities from attributionKeys.
const { data: report } = await governance.check.checkControllerCheck('cus-acme', {
  dimensions:      { teamId: 'team-eng', orgId: 'org-acme' },
  capabilityId:    'ai-tokens',
  requestedAmount: 1000,
});
Pass either entityIds or dimensions, not both.

Check request fields

FieldTypeRequiredDescription
entityIdsstring[]one ofEntity IDs to check. Min 1, max 100 per call.
dimensionsobjectone ofNon-empty key-value map. The engine resolves entities by matching keys against entity type attributionKeys.
capabilityIdstringyesThe capability to check. Must exist and be of type METER — returns 400 otherwise.
requestedAmountnumbernoAmount the caller intends to consume. Defaults to 1.

Check response

200 OK:
{
  "hasAccess": true,
  "checks": [
    {
      "entityId": "team-eng",
      "hasAccess": true,
      "chain": [
        {
          "entityId": "team-eng",
          "scopeEntityIds": [],
          "cadence": "P1M",
          "currentUsage": 42311,
          "usageLimit": 200000,
          "hasAccess": true
        },
        {
          "entityId": "org-acme",
          "scopeEntityIds": [],
          "cadence": "P1M",
          "currentUsage": 87450,
          "usageLimit": 1000000,
          "hasAccess": true
        }
      ]
    }
  ]
}
FieldDescription
hasAccessTop-level roll-up. true when every budget in every chain allows the request.
checks[].entityIdThe entity this check entry corresponds to.
checks[].hasAccessRoll-up for this entity’s chain.
checks[].chain[]Per-node budget entries from the target entity up to the root.
chain[].entityIdEntity at this node in the hierarchy.
chain[].scopeEntityIdsScope that matched. [] is the node-wide budget.
chain[].cadenceISO-8601 duration of the reset cadence (e.g., P1M).
chain[].currentUsageConsumed in the active cadence period.
chain[].usageLimitHard limit; null means usage is tracked but the limit never blocks.
chain[].hasAccesscurrentUsage + requestedAmount <= usageLimit.

Finding the binding constraint

const { data: report } = await governance.check.checkControllerCheck('cus-acme', {
  entityIds:       ['team-eng'],
  capabilityId:    'ai-tokens',
  requestedAmount: 500,
});

if (!report.hasAccess) {
  const denied = report.checks
    .flatMap(c => c.chain)
    .find(n => !n.hasAccess);

  return {
    allowed:   false,
    entityId:  denied?.entityId,
    remaining: denied ? (denied.usageLimit ?? 0) - denied.currentUsage : 0,
  };
}

Ingest

POST /owners/:ownerId/ingest Records one or more consumption events and increments the usage counter for each (entity, capability) pair. Returns 204 No Content immediately — ingest is fire-and-forget and never gates.

By entity IDs

await governance.ingest.ingestControllerIngest('cus-acme', {
  events: [
    { entityIds: ['team-eng'], capabilityId: 'ai-tokens', amount: 1250 },
  ],
});

By dimensions

await governance.ingest.ingestControllerIngest('cus-acme', {
  events: [
    {
      dimensions:   { teamId: 'team-eng', orgId: 'org-acme' },
      capabilityId: 'ai-tokens',
      amount:       1250,
    },
  ],
});

Batching events

Send up to 100 events in one request.
await governance.ingest.ingestControllerIngest('cus-acme', {
  events: [
    { entityIds: ['org-acme'],  capabilityId: 'api-calls', amount: 1 },
    { entityIds: ['team-eng'],  capabilityId: 'api-calls', amount: 1 },
    { entityIds: ['team-eng'],  capabilityId: 'ai-tokens', amount: 2500 },
  ],
});

Ingest event fields

FieldTypeRequiredDescription
entityIdsstring[]one ofEntity IDs to increment. Min 1, max 100 per event.
dimensionsobjectone ofNon-empty key-value map; engine resolves entities from attributionKeys.
capabilityIdstringyesThe capability to increment. Must exist and be of type METER.
amountnumberyesNon-negative integer. Amount to add. 0 is a valid no-op.

Full check-then-ingest pattern

async function consumeTokens(
  customerId: string,
  teamId:     string,
  tokenCount: number,
): Promise<{ allowed: boolean; remaining?: number }> {
  // 1. Check before consuming
  const { data: report } = await governance.check.checkControllerCheck(customerId, {
    entityIds:       [teamId],
    capabilityId:    'ai-tokens',
    requestedAmount: tokenCount,
  });

  if (!report.hasAccess) {
    const node = report.checks.flatMap(c => c.chain).find(n => !n.hasAccess);
    return { allowed: false, remaining: node ? (node.usageLimit ?? 0) - node.currentUsage : 0 };
  }

  // 2. Proceed with the operation …

  // 3. Ingest after consuming
  await governance.ingest.ingestControllerIngest(customerId, {
    events: [{ entityIds: [teamId], capabilityId: 'ai-tokens', amount: tokenCount }],
  });

  return { allowed: true };
}