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
| Field | Type | Required | Description |
|---|
entityIds | string[] | one of | Entity IDs to check. Min 1, max 100 per call. |
dimensions | object | one of | Non-empty key-value map. The engine resolves entities by matching keys against entity type attributionKeys. |
capabilityId | string | yes | The capability to check. Must exist and be of type METER — returns 400 otherwise. |
requestedAmount | number | no | Amount 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
}
]
}
]
}
| Field | Description |
|---|
hasAccess | Top-level roll-up. true when every budget in every chain allows the request. |
checks[].entityId | The entity this check entry corresponds to. |
checks[].hasAccess | Roll-up for this entity’s chain. |
checks[].chain[] | Per-node budget entries from the target entity up to the root. |
chain[].entityId | Entity at this node in the hierarchy. |
chain[].scopeEntityIds | Scope that matched. [] is the node-wide budget. |
chain[].cadence | ISO-8601 duration of the reset cadence (e.g., P1M). |
chain[].currentUsage | Consumed in the active cadence period. |
chain[].usageLimit | Hard limit; null means usage is tracked but the limit never blocks. |
chain[].hasAccess | currentUsage + 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
| Field | Type | Required | Description |
|---|
entityIds | string[] | one of | Entity IDs to increment. Min 1, max 100 per event. |
dimensions | object | one of | Non-empty key-value map; engine resolves entities from attributionKeys. |
capabilityId | string | yes | The capability to increment. Must exist and be of type METER. |
amount | number | yes | Non-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 };
}