Concepts

Consumption tracking

Record and query per-user per-application numeric counters in auth-service.

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:

FieldTypeDescription
idUUIDPrimary key
userIdstringUser identifier
applicationIdUUIDApplication identifier
keystringCounter name, e.g. api.calls, tokens.used
valuenumericNumeric delta (positive or negative)
recordedAttimestampRecord creation time

Consumption aggregates

In the same transaction as the entry insert, an upsert maintains a running total:

FieldTypeDescription
userIdstringUser identifier
applicationIdUUIDApplication identifier
keystringCounter name
totalnumericRunning total (sum of all entries)
updatedAttimestampLast 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:

  1. Bearer access token — an access token issued via the client_credentials grant, verified by Auth Service. It is bound to its own application: a token used for another application's applicationId gets 403 AUTH_011. A user-bound token gets 403 CONS_004; an invalid or expired token gets 401 AUTH_001.
  2. Admin session cookie — a valid session for a user with the admin or superadmin global 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,
  }),
});
Copyright © 2026