Consumption tracking
Auth Service provides a lightweight consumption accounting API for recording arbitrary numeric counters per user per application. Common use cases: API call counts, credit balances, storage usage, or token spend. A key that prices a metered feature of the user's plan follows the metered path instead.
Data model
Consumption entries
Every call to POST /api/consumption for a non-metered key inserts a raw entry:
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
userId | string | User identifier |
applicationId | UUID | Application identifier |
key | string | Counter name, e.g. api.calls, tokens.used |
value | numeric | Numeric delta (positive or negative) |
recordedAt | timestamp | Record creation time |
Consumption aggregates
In the same transaction as the entry insert, an upsert maintains a running total:
| Field | Type | Description |
|---|---|---|
userId | string | User identifier |
applicationId | UUID | Application identifier |
key | string | Counter name |
total | numeric | Running total (sum of all entries) |
updatedAt | timestamp | Last update time |
The aggregate supports negative values — credits can be debited by recording a negative value.
Key format
Keys must match /^[a-zA-Z0-9.]+$/ (alphanumeric with dots). Examples: api.calls, tokens.used, storage.gb.
Authentication
The consumption endpoints accept two forms of authentication:
- Bearer access token — an access token issued via the
client_credentialsgrant, verified by Auth Service. It is bound to its own application: a token used for another application'sapplicationIdgets403 AUTH_011. A user-bound token gets403 CONS_004; an invalid or expired token gets401 AUTH_001. - Admin session cookie — a valid session for a user with the
adminorsuperadminglobal role. It may act on any application.
All other callers receive 403 CONS_004. Resetting a counter accepts the admin session only.
API endpoints
Record consumption
POST /api/consumption
Authorization: Bearer <access_token>
{
"userId": "<user_id>",
"applicationId": "<app_uuid>",
"key": "api.calls",
"value": 1
}
The userId and applicationId pair must correspond to an existing userApplications row (404 CONS_003 otherwise). An invalid key returns 400 CONS_001 and a non-finite value 400 CONS_002. An optional idempotencyKey (1–200 chars) is honoured for metered keys only.
Response:
{
"success": true,
"aggregate": {
"key": "api.calls",
"total": "42"
}
}
Metered keys
When key is the featureKey of a metered price on the plan of the user's active subscription, the report goes through the billing path instead of the counters above: value must be greater than zero (400 BIL_002), usage is accumulated per UTC month, a report that would cross the price's usageLimit is refused with 403 BIL_003 (-1 is unlimited), and the response carries "metered": true with the month's total as aggregate.total. When Stripe is configured the usage is also sent to a Stripe meter. Metered usage is not returned by the GET and DELETE consumption endpoints. See Recording consumption and the Stripe integration guide.
Get aggregates for a user + app
GET /api/consumption/:userId/:applicationId
Returns all aggregate counters for the given user/application pair:
{
"aggregates": [
{ "key": "api.calls", "total": "42", "updatedAt": "..." },
{ "key": "tokens.used", "total": "1500", "updatedAt": "..." }
]
}
GET /api/consumption/:userId/:applicationId/:key returns { "aggregate": { "key", "total", "updatedAt" } } for one key, or 404 CONS_003 when nothing was recorded.
Reset a counter
DELETE /api/consumption/:userId/:applicationId/:key
Removes the aggregate row and all raw entry rows for the given key and returns 204. Admin session required.
Admin consumption endpoints
Administrators can read consumption across all users of an application:
GET /api/admin/applications/:appId/consumption
GET /api/admin/applications/:appId/consumption/monthly?year=2026&month=10
The first returns { "entries": [...] } with the aggregates of all users of the application (user name and email, plan id, key, total, updatedAt); the second returns { "entries": [...] } with the raw entries of a calendar month summed per user and key. year and month default to the current ones, and the month boundaries use the server's local time zone.
Backend service pattern
A backend service using the client_credentials grant to record consumption:
// 1. Obtain an access token (cache this and refresh before expiry)
const basic = Buffer.from(`my-app:${process.env.APP_CLIENT_SECRET!}`).toString("base64");
const tokenRes = await fetch("https://auth.example.com/api/auth/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${basic}`, // default for confidential clients; TOKEN_ENDPOINT_AUTH_METHOD=post switches non-system applications to client_secret_post
},
body: new URLSearchParams({
grant_type: "client_credentials",
resource: "https://api.example.com", // the application url: yields a JWT
}),
});
const { access_token } = await tokenRes.json();
// 2. Record a consumption event
await fetch("https://auth.example.com/api/consumption", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${access_token}`,
},
body: JSON.stringify({
userId: req.user.sub,
applicationId: "app-uuid",
key: "api.calls",
value: 1,
}),
});