Entitlements
Usage & Thresholds
The two numbers on the grant that decide enforcement, the allowance arithmetic, the exact refusal, the three crossing signals, and how every report and every closed window lands in the audit trail.
Two numbers on the grant
Each NUMBER grant — the row that attaches an entitlement to a License — carries two numbers, and they alone decide what is accepted:
limitCapExceededOveragePercent | The grant is | Maximum allowed usage | A report is refused when |
|---|---|---|---|
-1 | unlimited — its value is the sentinel -1 | none | never |
0 | a hard cap | value | the updated total would exceed value |
> 0 | a soft limit with a bounded allowance | value + value × percent / 100 | the updated total would exceed the maximum |
The percentage is set when the entitlement is granted (POST /licenses/{slug}/entitlements) or changed (PUT /licenses/{slug}/entitlements/{entitlementSlug}); omitted, it defaults to -1 for an unlimited value and 0 otherwise. It exists only on numeric grants — a BOOLEAN or CONFIG grant has none, and its response omits the field — and it must be -1 exactly when the value is -1, otherwise 0 or more. NUMBER_AI_CREDIT grants follow the same rules as NUMBER.
Because the numbers live on the grant, one entitlement is enforced differently on every License. Kaiten Sushi Shop's menu-items, across the versions its three License families serve:
| License version | value | limitCapExceededOveragePercent | Maximum allowed usage | Refused from |
|---|---|---|---|---|
Starter (starter) | 5 | 0 | 5 | 6 |
Standard (standard) | 10 | 10 | 11 | 12 |
Premium (premium-v2) | 50 | 20 | 60 | 61 |
Premium's locations is granted -1 / -1: no cap, no allowance, no refusal — while Starter's locations is a hard cap of 1. A grant belongs to one version, so versions of a family can differ: Premium's draft premium-v3 grants 100 menu items, and its archived first version premium grants five delivery drivers where premium-v2 grants ten.
The refusal
Every report is one comparison: the total the report would produce — append folded through the aggregation method, or set — against the grant's maximum allowed usage. Above it, the report is refused with 409 Conflict and not written: the stored total does not move, and the refusal is recorded as ENTITLEMENT_USAGE_REPORT_REJECTED. Up to the maximum, overage included, it is accepted and written.
Ninja Osaka's instance holds 5 of 5 menu items on Starter. Publishing a sixth dish — menu-items is a LATEST gauge, so the report states the new count:
curl -i -X POST "$KAITEN_API/instances/ninja-osaka-prod/entitlements/menu-items/usage" \
-H "Authorization: Bearer $KAITEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "value": { "type": "number", "value": 6 }, "behavior": "append" }'HTTP/1.1 409 Conflict
Content-Type: application/problem+json{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.8",
"title": "Conflict",
"status": 409,
"code": "ReportEntitlementUsageMetric.ThresholdExceeded",
"detail": "Usage for menu-items exceeds the allowed threshold",
"instance": "/api/instances/ninja-osaka-prod/entitlements/menu-items/usage"
}The body follows RFC 9457 and carries a stable machine-readable code next to these fields. Read the usage back and it is still 5 — and the same report against demo-restaurant-prod, whose Standard grant is 10 with a 10 % allowance, is accepted at 11 and refused at 12.
A set is compared the same way: a correction that lands at or below the maximum is accepted, a downward one included, and a set that lands above it is refused like any other report.
Kaiten enforces at the report boundary. What a refusal means for the end user — a disabled checkout, an upgrade prompt, a queued order — is your product's decision; the 409 and the ENTITLEMENT_USAGE_REPORT_REJECTED entry are the facts it can build on.
The three crossing signals
Below the maximum, three events mark the way up. The two crossing events fire once per crossing — on the report that crosses, never on later reports that stay above; the cap-reached event fires on every report whose total lands exactly on the value. None fires for an unlimited grant.
| Event | Fires when | Payload |
|---|---|---|
INSTANCE_ENTITLEMENT_USAGE_WARNING_THRESHOLD_REACHED | The total crosses the warning boundary — value × warningThresholdPercent / 100. The percentage is on the entitlement, the same for every License; 0 (the default) disables it. | entitlement_slug, threshold (the granted value), boundary, value |
INSTANCE_ENTITLEMENT_USAGE_REACHED | The total lands exactly on the granted value — on every report that does, so a LATEST gauge re-reporting its cap fires it again | The usage record, as the usage endpoints return it |
INSTANCE_ENTITLEMENT_CAP_EXCEEDED | The total crosses the granted value upward while staying inside the allowance of a soft limit | entitlement_slug, threshold, value, overage (value − threshold) |
The warning boundary is the one threshold that stays on the catalog: warningThresholdPercent describes how menu-items is measured, the same for every License, and its arithmetic mirrors the allowance's so that the two boundaries stay consistent. Kaiten Sushi Shop's seeded catalog leaves it at 0 on every NUMBER entitlement, so no warning fires on the seeded data until you set one.
Worked example — Demo Restaurant's menu
demo-restaurant-prod runs on Standard: menu-items granted 10 with a 10 % allowance, currently at 6. Give the entitlement an 80 % warning threshold first. PUT replaces the entitlement's details, so resend its name, description and presentation — an omitted icon, unit label, userFacing or displayOrder is reset — and echo its type and aggregation method; leaving groupSlugs out keeps its group:
curl -i -X PUT "$KAITEN_API/entitlements/menu-items" \
-H "Authorization: Bearer $KAITEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Menu Items",
"description": "Number of dishes a restaurant can publish on its menu.",
"type": "NUMBER",
"aggregationMethod": "LATEST",
"icon": "lucide:utensils",
"unitSingular": "menu item",
"unitPlural": "menu items",
"userFacing": true,
"displayOrder": 1,
"warningThresholdPercent": 80
}'HTTP/1.1 204 No Content. The restaurant then publishes dishes one batch at a time (LATEST: each report states the count):
Report 9 → 9 / 10 accepted crosses the warning boundary (8) → INSTANCE_ENTITLEMENT_USAGE_WARNING_THRESHOLD_REACHED
Report 10 → 10 / 10 accepted lands exactly on the value → INSTANCE_ENTITLEMENT_USAGE_REACHED
Report 11 → 11 / 10 accepted crosses the value, inside 11 → INSTANCE_ENTITLEMENT_CAP_EXCEEDED · 110 % of the value, 100 % of the maximum
Report 12 → refused 409 would exceed the maximum (11) → ENTITLEMENT_USAGE_REPORT_REJECTED · total stays 11Every accepted line also emits ENTITLEMENT_USAGE_REPORT_ACCEPTED. On Starter the same sequence stops at 5 / 5: Ninja Osaka's grant has no allowance, so 6 is already refused. On Premium's unlimited locations, nothing in this table can happen.
Two boundaries matter — the granted value and the maximum allowed usage — and a meter can be scaled on either. The console measures against the maximum: its meter stops at 100 % where the next report is refused, a tick marks the granted value, and usage past it carries the In allowance badge, then Limit reached at the allowance's edge — Demo Restaurant's 11 of 10 reads 11 / 10 (+10% overage) at 100 %. @kaitencloud/client and the Python SDK express usage as a percentage of the granted value: 100 % where the customer has consumed what they bought, 110 % at 11 of 10, with the maximum allowed usage beside it — so a host can tell "at the included value but still served" from "the next report will be refused". The Go SDK returns the value, the limit and the window bounds, and computes no percentage.
Events
| Event name | Event type | When |
|---|---|---|
ENTITLEMENT_USAGE_REPORT_ACCEPTED | com.kaiten.instance.entitlement.v1.usage_report_accepted | A report is written — payload entitlement_slug, value, behavior, event_count, status: ACCEPTED |
ENTITLEMENT_USAGE_REPORT_REJECTED | com.kaiten.instance.entitlement.v1.usage_report_rejected | A report is refused above the maximum allowed usage — same payload, status: REJECTED; nothing is written |
INSTANCE_ENTITLEMENT_USAGE_WARNING_THRESHOLD_REACHED | com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached | The total crosses the warning boundary |
INSTANCE_ENTITLEMENT_USAGE_REACHED | com.kaiten.instance.entitlement.v1.usage_reached | The total lands exactly on the granted value |
INSTANCE_ENTITLEMENT_CAP_EXCEEDED | com.kaiten.instance.entitlement.v1.cap_exceeded | The total crosses the granted value inside the allowance |
INSTANCE_ENTITLEMENT_USAGE_PERIOD_ROLLED_OVER | com.kaiten.instance.entitlement.v1.usage_period_rolled_over | A periodic counter closes a window — one event per closed window, synthetic ones included (Metering) |
ENTITLEMENT_VALUE_GET | com.kaiten.instance.entitlement.v1.value_get | One entitlement's usage is read |
Two delivery paths, one stream: on Kaiten Cloud Kaiten Cloud subscribe a webhook endpoint to these events; on a self-hosted stack, consume them from RabbitMQ. The event catalog carries every payload.
The audit trail
Every event above is also recorded in the instance's audit trail, append-only: the report that was accepted, the report that was refused, the crossing, the closed window — including the synthetic windows a rollover materializes for the periods an instance skipped, so a monthly counter's history reads without gaps. The trail is the read side of the same record the events are the write side of; it is how a disputed refusal is answered after the fact.
curl "$KAITEN_API/instances/ninja-osaka-prod/audit-trails?event_name=ENTITLEMENT_USAGE_REPORT_REJECTED&after=2026-09-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer $KAITEN_TOKEN"{
"items": [
{
"id": "0d6e2f5a-4b1c-4c6e-9a0f-2f8f4f1c9d3e",
"instanceSlug": "ninja-osaka-prod",
"eventName": "ENTITLEMENT_USAGE_REPORT_REJECTED",
"eventType": "com.kaiten.instance.entitlement.v1.usage_report_rejected",
"timestamp": "2026-09-08T09:41:03Z",
"payload": { "entitlement_slug": "menu-items", "value": 6, "behavior": "append", "event_count": 5, "status": "REJECTED" }
}
],
"hasMore": false
}| Parameter | Type | Meaning |
|---|---|---|
event_name | string | Exact catalog name — ENTITLEMENT_USAGE_REPORT_REJECTED, not the dotted type |
after / before | RFC 3339 | Inclusive time bounds |
limit | integer | 1–200, default 50 |
cursor | string | The previous page's nextCursor |
Reading the trail requires read:instances. Answer who had access walks the query side in full, including the cursor walk that hands a complete window to an auditor.
Where to go next
The hands-on version — a hard cap, an allowance, an unlimited grant, and a corrected counter on the seeded instances: Meter usage and enforce limits. The one-screen version: Usage & Metering. Exact reference — the grant's two CHECK constraints: the license_entitlement table.
Metering
How a NUMBER entitlement is measured — the six aggregation methods, append versus set, what happens inside a usage report, and the periodic usage windows that reset a counter.
Overview
Components, releases, deployment zones, and the append-only deployment journal — the runtime pillar of the control plane, and why it sits next to the contract.