Usage
Per-key sparklines, per-tenant counts and the billable-tenant figure.
packages/api/src/resources/usage.controller.ts by packages/docs/tools/generate-reference.mjs. Every example is a real exchange recorded against the control plane by packages/api/src/testing/reference-examples.spec.ts, with ids from that run. Credentials in the formats this product issues (kr_, krsk_, krses_, JWTs) and values under credential-named fields (password, *_secret, *_token, *_key, lookup_hash) are elided at record time; the recorded file and every page of this site are swept for credential shapes (kr_, krsk_, krses_, JWTs, URLs carrying a password, password fields) before either is accepted. Do not edit by hand: src/reference.spec.ts regenerates it and fails on a difference.What the dashboard reads about usage, and nothing else reads.
The whole file is GET, and that is the point: the two figures here are
written by week 5's rollup and by the ingest path, and a second writer to
either is what migration 0008's header calls "how an invoice becomes wrong".
The division between the two endpoints is the one that matters:
billingiskeyring.billable_tenant_monthread straight through. That view is the single definition of the billing unit -- one customer organisation per workspace, live only, counted across projects -- and it is deliberately not recomputed here fromtenant_activity_month, which is the same mistake in a different building.tenantsreportsrequest_countper tenant, project and environment. Those are dimensions the rollup keeps so per-project and per-environment usage stays available without changing what is billed, so this endpoint carries no billable flag and no total of its own. A caller that wants the number on the invoice asks the other endpoint.
GET /v1/keys/:id/usage
The per-key sparkline, report section 11.3's week-6 row, on the vendor
plane. The embed plane has its own (TenantScopedRepository.dailyUsage)
and the two are deliberately separate statements: that one carries four
predicates bound from a signed token and has no argument a handler could
widen, which is the property embed-isolation.spec.ts asserts. Sharing a
query builder between the two planes would be the one change that could
take it away.
sample_rate is divided out for the same reason it is there: a chart that
counted rows would under-report a sampled fleet by exactly the sampling
factor.
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Path parameters. id.
Query. Validated by this schema, from the control plane's own source:
const KeyUsage = z.object({
days: z.coerce.number().int().min(1).max(90).default(30),
});Example.
curl https://keyring-api.belghalem.fr/v1/keys/01a0ade7-b916-78a6-801e-79f4ba8233fb/usage?days=7 \
-H "Authorization: Bearer $KEYRING_SECRET_KEY"HTTP/1.1 200 OK
Content-Type: application/json
{
"object": "usage",
"key_id": "01a0ade7-b916-78a6-801e-79f4ba8233fb",
"days": 7,
"data": [
{
"day": "2026-09-17",
"requests": 2,
"errors": 0
}
]
}GET /v1/usage/billing
The view this reads is live-only by definition, so a test credential is
refused rather than filtered; assertBillingReadable carries the reasoning.
month is formatted in SQL for the same reason the sparkline's day is:
node-pg parses a Postgres date into a Date at local midnight, so
.toISOString() on any host east of UTC rolls the label back a day -- and
since the value is always the first of a month, back a whole month. Every EU
host that does not set TZ=UTC is east of it.
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Query. Validated by this schema, from the control plane's own source:
const Billing = z.object({
months: z.coerce.number().int().min(1).max(24).default(6),
});Example.
curl https://keyring-api.belghalem.fr/v1/usage/billing?months=3 \
-H "Authorization: Bearer $KEYRING_SECRET_KEY"HTTP/1.1 200 OK
Content-Type: application/json
{
"object": "list",
"data": [
{
"object": "billable_tenant_month",
"month": "2026-09-01",
"active_tenants": 1
}
]
}GET /v1/usage/tenants
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Query. Validated by this schema, from the control plane's own source:
const TenantUsage = z.object({
months: z.coerce.number().int().min(1).max(24).default(1),
env: Env.optional(),
});Example.
curl https://keyring-api.belghalem.fr/v1/usage/tenants?months=1 \
-H "Authorization: Bearer $KEYRING_SECRET_KEY"HTTP/1.1 200 OK
Content-Type: application/json
{
"object": "list",
"data": [
{
"object": "tenant_activity_month",
"tenant_id": "01a0ade7-b909-7ae2-9477-aa287f446aad",
"project_id": "01a0ade7-b8fb-7711-bb1a-7d1bd5962995",
"env": "test",
"month": "2026-09-01",
"request_count": 2,
"first_seen_at": "2026-09-17T05:47:20.053Z",
"last_seen_at": "2026-09-17T05:47:20.053Z"
},
{
"object": "tenant_activity_month",
"tenant_id": "01a0ade7-b909-7ae2-9477-aa287f446aad",
"project_id": "01a0ade7-b8fb-7711-bb1a-7d1bd5962995",
"env": "live",
"month": "2026-09-01",
"request_count": 1,
"first_seen_at": "2026-09-17T05:47:20.053Z",
"last_seen_at": "2026-09-17T05:47:20.053Z"
}
]
}Shared validators
Defined once in packages/api/src/validation.ts and used by the schemas above.
const Env = z.enum(['live', 'test']);