Usage & Meters API
The usage API tracks consumption-based metrics through meters — configurable counters that aggregate usage events over time. Each meter defines how events are aggregated, when counters reset, and whether quota limits are enforced.
Base path: /api/v1/tenant/{tenantId}/...Middleware: auth:sanctum → tenant.resolve → tenant.member → onboarding.complete
Key Concepts
Meters
A meter is a named counter tied to a feature code. It defines:
| Property | Description |
|---|---|
code | Stable identifier (e.g., api-requests, storage-gb) |
aggregation_type | How events are combined: sum, max, count, last_value |
reset_interval | When the counter resets: monthly, weekly, daily, none |
quota_enforcement | How limits are applied: none, soft, hard |
unit_label | Display label (e.g., "requests", "GB") |
Quota Enforcement Levels
| Level | Behavior |
|---|---|
none | No enforcement — usage is tracked but never blocked |
soft | Usage is tracked and alerts are generated, but requests are not blocked |
hard | Usage is blocked with a 429 response when the quota is exceeded |
Quota limits are resolved from the tenant's active subscription. Each meter's feature_code is matched against the plan's entitlements to determine the limit. If no matching entitlement exists or the entitlement is boolean, the quota is treated as unlimited.
Aggregation Types
| Type | Description |
|---|---|
sum | Total of all event quantities in the period |
max | Highest single event quantity in the period |
count | Number of events recorded (ignores quantity) |
last_value | Most recent event quantity |
Get Usage Summary
GET /api/v1/tenant/{tenantId}/usage
Returns a summary of current usage across all active meters for the tenant.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"data": {
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"period_start": "2026-03-01T00:00:00.000000Z",
"period_end": "2026-03-31T23:59:59.000000Z",
"meters": [
{
"meter_id": "660e8400-e29b-41d4-a716-446655440001",
"meter_code": "api-requests",
"meter_name": "API Requests",
"current_usage": 7500.0,
"quota_limit": 10000,
"usage_percent": 75.0,
"aggregation_type": "sum",
"reset_interval": "monthly",
"quota_enforcement": "hard",
"unit_label": "requests"
},
{
"meter_id": "660e8400-e29b-41d4-a716-446655440002",
"meter_code": "storage-gb",
"meter_name": "Storage",
"current_usage": 2.5,
"quota_limit": null,
"usage_percent": null,
"aggregation_type": "last_value",
"reset_interval": "none",
"quota_enforcement": "none",
"unit_label": "GB"
}
]
}
}
| Field | Type | Description |
|---|---|---|
tenant_id | UUID | Tenant identifier |
period_start | datetime | Start of current usage period |
period_end | datetime | End of current usage period |
meters | array | Usage summary per active meter |
Per-meter fields:
| Field | Type | Description |
|---|---|---|
meter_id | UUID | Meter identifier |
meter_code | string | Stable meter code |
meter_name | string | Display name |
current_usage | float | Aggregated usage for the current period |
quota_limit | int|null | Maximum allowed (null = unlimited) |
usage_percent | float|null | Percentage used (null if unlimited) |
aggregation_type | enum | sum, max, count, last_value |
reset_interval | enum | monthly, weekly, daily, none |
quota_enforcement | enum | none, soft, hard |
unit_label | string|null | Unit display label |
reset_interval. Monthly meters align with the subscription's billing period. Weekly and daily meters calculate from the current date. Meters with reset_interval: "none" accumulate indefinitely.Get Quota Status
GET /api/v1/tenant/{tenantId}/usage/quotas
Returns the quota status for all active meters. This endpoint is designed for building quota warning UI — each meter includes a pre-classified status field.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"data": {
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"meters": [
{
"meter_id": "660e8400-e29b-41d4-a716-446655440001",
"meter_code": "api-requests",
"meter_name": "API Requests",
"current_usage": 9500.0,
"quota_limit": 10000,
"usage_percent": 95.0,
"status": "warning",
"quota_enforcement": "hard",
"unit_label": "requests"
},
{
"meter_id": "660e8400-e29b-41d4-a716-446655440002",
"meter_code": "storage-gb",
"meter_name": "Storage",
"current_usage": 2.5,
"quota_limit": null,
"usage_percent": null,
"status": "ok",
"quota_enforcement": "none",
"unit_label": "GB"
}
]
}
}
Status values:
| Status | Condition | Description |
|---|---|---|
ok | Usage < 80% of quota | Normal usage |
warning | Usage >= 80% and < 100% | Approaching the limit |
exceeded | Usage >= 100% of quota | Quota has been reached or exceeded |
Meters with unlimited quotas (quota_limit: null) always report status: "ok".
Get Meter Details
GET /api/v1/tenant/{tenantId}/usage/{meterCode}
Returns detailed usage information for a specific meter, including recent events.
Auth: Bearer token + tenant member + onboarding complete
Path Parameters:
| Param | Type | Description |
|---|---|---|
meterCode | string | The meter's code identifier (e.g., api-requests) |
Response (200):
{
"data": {
"meter_id": "660e8400-e29b-41d4-a716-446655440001",
"meter_code": "api-requests",
"meter_name": "API Requests",
"current_usage": 7500.0,
"quota_limit": 10000,
"usage_percent": 75.0,
"aggregation_type": "sum",
"reset_interval": "monthly",
"quota_enforcement": "hard",
"unit_label": "requests",
"period_start": "2026-03-01T00:00:00.000000Z",
"period_end": "2026-03-31T23:59:59.000000Z",
"recent_events": [
{
"id": "770e8400-e29b-41d4-a716-446655440010",
"quantity": 500.0,
"recorded_at": "2026-03-27T14:30:00.000000Z",
"metadata": {
"endpoint": "/api/v1/data/export",
"user_id": "42"
}
},
{
"id": "770e8400-e29b-41d4-a716-446655440011",
"quantity": 150.0,
"recorded_at": "2026-03-27T10:15:00.000000Z",
"metadata": null
}
]
}
}
The recent_events array contains up to 20 of the most recent usage events for the current period, sorted by recorded_at descending.
Response (404):
{
"message": "Meter not found: invalid-code"
}
Record Usage Event
POST /api/v1/tenant/{tenantId}/usage
Records a new usage event against a meter. Supports idempotent submissions via the idempotency_key field.
Auth: Bearer token + tenant member + onboarding complete
Request Body:
{
"meter_code": "api-requests",
"quantity": 500,
"idempotency_key": "batch-2026-03-27-export-42",
"recorded_at": "2026-03-27T14:30:00Z",
"metadata": {
"endpoint": "/api/v1/data/export",
"user_id": "42"
}
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
meter_code | string | Yes | — | Code of the target meter (max 255 chars) |
quantity | number | No | 1 | Usage amount (min: 0) |
idempotency_key | string | No | null | Unique key to prevent duplicate recording (max 255 chars) |
recorded_at | datetime | No | now | When the usage occurred (ISO-8601) |
metadata | object | No | null | Arbitrary key-value data attached to the event |
Response (201):
{
"message": "Usage recorded successfully",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440010",
"meter_code": "api-requests",
"quantity": 500.0,
"recorded_at": "2026-03-27T14:30:00.000000Z"
}
}
Idempotency
The idempotency_key field prevents duplicate event recording. If you submit two requests with the same idempotency_key for the same meter, the second request returns a 409 Conflict:
{
"message": "Duplicate usage event (idempotency key already exists)"
}
Idempotency is enforced via a database unique constraint on (meter_id, idempotency_key). This is safe against race conditions — even concurrent requests with the same key will result in at most one recorded event.
Quota Enforcement
When a meter has quota_enforcement: "hard", the API checks whether recording the event would exceed the quota. If so, the event is rejected with a 429 response:
{
"message": "Quota exceeded for api-requests: 9500/10000",
"code": "QUOTA_EXCEEDED"
}
The quota check happens before the event is created — no partial recording occurs.
Meters with soft enforcement record the event and generate alerts but never reject requests. Meters with none enforcement have no quota checking.
Error Responses
| Status | Code | Description |
|---|---|---|
| 201 | — | Usage event recorded successfully |
| 404 | — | Meter not found (code does not exist or meter is inactive) |
| 409 | — | Duplicate idempotency key for this meter |
| 422 | — | Validation error (invalid meter_code, negative quantity, etc.) |
| 429 | QUOTA_EXCEEDED | Hard quota exceeded — event was not recorded |
Get Cost Estimate
GET /api/v1/tenant/{tenantId}/usage/cost-estimate
Returns a shadow-calculated estimate of usage costs for the current billing period. This estimate is based on local meter data and unit prices — actual billing is handled by Stripe and may differ due to prorations, taxes, or pricing changes.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"data": {
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"period_start": "2026-03-01T00:00:00.000000Z",
"period_end": "2026-03-31T23:59:59.000000Z",
"total_amount_cents": 7500,
"currency": "EUR",
"is_estimate": true,
"lines": [
{
"meter_code": "api-requests",
"meter_name": "API Requests",
"quantity": 7500.0,
"unit_price_cents": 1,
"amount_cents": 7500,
"currency": "EUR",
"unit_label": "requests"
}
]
}
}
| Field | Type | Description |
|---|---|---|
total_amount_cents | int | Sum of all line amounts |
currency | string | Currency from subscription or tenant preference |
is_estimate | boolean | Always true — actual billing may differ |
lines | array | Per-meter cost breakdown |
Per-line fields:
| Field | Type | Description |
|---|---|---|
meter_code | string | Meter identifier |
meter_name | string | Display name |
quantity | float | Aggregated usage for the period |
unit_price_cents | int | Price per unit in cents |
amount_cents | int | quantity × unit_price_cents |
currency | string | ISO 4217 currency code |
unit_label | string|null | Unit display label |
stripe_price_id) appear in the cost estimate. Meters without Stripe pricing are excluded even if they track usage.The cost estimate returns an empty lines array if the tenant has no active subscription or no billable meters.
Usage Enums Reference
Aggregation Type
| Value | Description |
|---|---|
sum | Total of all event quantities in the period |
max | Highest single event quantity |
count | Number of events (ignores quantity values) |
last_value | Most recent event quantity |
Defined in backend/app/Domain/Usage/Enums/AggregationType.php.
Reset Interval
| Value | Description |
|---|---|
monthly | Resets at the start of each billing month (aligned to subscription period) |
weekly | Resets every 7 days |
daily | Resets every 24 hours |
none | Never resets — cumulative across all time |
Defined in backend/app/Domain/Usage/Enums/ResetInterval.php.
Quota Enforcement
| Value | Description |
|---|---|
none | No enforcement — usage tracked only |
soft | Alerts generated but requests allowed |
hard | Requests blocked with 429 when quota exceeded |
Defined in backend/app/Domain/Usage/Enums/QuotaEnforcement.php.
What's Next
- Tenants & Teams API — Tenant management, team members, and invitations
- Pricing Models — How usage-based pricing is configured
- Subscriptions API — Subscription and entitlement endpoints
Invoices API
Tenant invoice endpoints: listing with status filtering, single invoice details with line items, and PDF download via proxied streaming.
Tenants & Teams API
Tenant management, team member CRUD, invitation lifecycle (create, accept, revoke, resend), role management, team stats, and admin tenant endpoints.