Usage Tracking
Guide to implementing usage tracking and reporting for license compliance and analytics.
Overview
Usage tracking lets you increment named usage counters against a license and check them against the limits set when the license was issued. It's deliberately simple: one metric per call, and the metric name must already be a key in that license's limits object — there's no way to track an ad-hoc metric the license wasn't issued with.
Allowed metric names
The full set of limit keys the server recognizes (set via limits when issuing a license, and the only valid metric values for tracking) is: users, seats, admins, projects, environments, tenants, api_calls_per_day, rate_limit_rps, concurrent_sessions, features. Anything else is rejected.
Basic Usage Tracking
Reporting a Single Metric
curl -X POST https://your-license-api.com/v1/track-usage \
-H "Content-Type: application/json" \
-d '{
"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28",
"token": "9f2c1e4a7b3d6f805c1a2b3d4e5f6789",
"metric": "api_calls_per_day",
"increment": 1
}'token is the opaque activation token returned by POST /v1/activate-license — proof the caller holds a real activation for this key, not just the key itself (the key alone isn't a secret; it's meant to live in a distributed app or public page source). increment is optional and defaults to 1. It adds to the metric's running total server-side — it isn't an absolute value.
Response:
{
"ok": true,
"metric": "api_calls_per_day",
"usage": 1501
}usage is the new running total for that metric after applying the increment, not a percentage or a snapshot of every metric.
Error responses:
| Status | Body | Cause |
|---|---|---|
| 404 | {"error": "License key not found"} | Key doesn't exist |
| 403 | {"error": "License is not active"} | Revoked or otherwise inactive |
| 403 | {"error": "License is expired"} | Past expires_at |
| 403 | {"error": "Invalid activation token"} | token doesn't match a real activation for this key |
| 403 | {"error": "Metric not allowed: <metric>"} | metric isn't a key in this license's limits |
| 403 | {"error": "Usage limit exceeded for metric: <metric>"} | The increment would push usage past the metric's limit |
Note the limit-exceeded case is a hard rejection — the increment is not applied, and usage is not returned. To track multiple metrics for the same event (e.g. an API call that also counts toward a per-tenant quota), make one track-usage call per metric.
Usage Tracking Implementation
Basic Usage Tracker
class UsageTracker {
constructor(licenseKey, activationToken, apiUrl) {
this.licenseKey = licenseKey;
this.activationToken = activationToken; // from POST /activate-license
this.apiUrl = apiUrl;
}
// Report usage for a single metric. `metric` must already be a key in
// this license's `limits` (see the allowed-metrics list above).
// Requires the activation token this instance was issued by
// /activate-license -- the key alone is not accepted.
async trackUsage(metric, increment = 1) {
const response = await fetch(`${this.apiUrl}/track-usage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: this.licenseKey,
token: this.activationToken,
metric,
increment
}),
timeout: 30000
});
const result = await response.json();
if (!response.ok) {
console.warn(`Usage tracking failed for ${metric}: ${result.error}`);
return null;
}
return result; // { ok, metric, usage }
}
}
// Usage -- activationToken comes from a prior POST /activate-license call
const tracker = new UsageTracker(
process.env.LICENSE_KEY,
activationToken,
'https://your-license-api.com/v1'
);
await tracker.trackUsage('api_calls_per_day');
await tracker.trackUsage('concurrent_sessions', 3);Batching Local Increments
Since each API call reports exactly one metric, high-frequency events (e.g. per-request API-call tracking) are usually better batched locally and flushed periodically, rather than calling track-usage on every single event:
class BatchedUsageTracker extends UsageTracker {
constructor(licenseKey, activationToken, apiUrl, flushIntervalMs = 60000) {
super(licenseKey, activationToken, apiUrl);
this.pending = new Map(); // metric -> accumulated increment
this.timer = setInterval(() => this.flush(), flushIntervalMs);
}
// Accumulate locally instead of calling the API immediately
record(metric, amount = 1) {
this.pending.set(metric, (this.pending.get(metric) || 0) + amount);
}
async flush() {
const toSend = Array.from(this.pending.entries());
this.pending.clear();
for (const [metric, increment] of toSend) {
const result = await this.trackUsage(metric, increment);
if (!result) {
// Failed -- put it back for the next flush rather than losing it
this.pending.set(metric, (this.pending.get(metric) || 0) + increment);
}
}
}
stop() {
clearInterval(this.timer);
return this.flush(); // final flush
}
}
// Usage
const batched = new BatchedUsageTracker(
process.env.LICENSE_KEY,
activationToken,
'https://your-license-api.com/v1'
);
// Called on every API request, but only sent to the server once a minute
app.use((req, res, next) => {
batched.record('api_calls_per_day');
next();
});Checking Usage Status
GET-ing usage back isn't how this works — POST /usage-report returns a current snapshot of every metric the license has limits for, not a call you make to track something:
curl -X POST https://your-license-api.com/v1/usage-report \
-H "Content-Type: application/json" \
-d '{"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28"}'Response:
{
"valid": true,
"status": "active",
"expires_at": "2025-12-31T23:59:59.000Z",
"metrics": [
{
"metric": "api_calls_per_day",
"used": 1501,
"limit": 10000,
"remaining": 8499,
"exceeded": false
},
{
"metric": "users",
"used": 25,
"limit": 100,
"remaining": 75,
"exceeded": false
}
]
}This is a live snapshot at the moment you call it — there's no date-range, granularity, or historical time-series option, and no CSV/PDF export. If you need historical usage trends, you'll need to poll this endpoint on your own schedule and store the results yourself.
class UsageStatusChecker {
constructor(licenseKey, apiUrl) {
this.licenseKey = licenseKey;
this.apiUrl = apiUrl;
}
async getUsageReport() {
const response = await fetch(`${this.apiUrl}/usage-report`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: this.licenseKey }),
timeout: 30000
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Usage report failed: ${error.error}`);
}
return response.json();
}
async checkForWarnings(thresholdPercent = 80) {
const report = await this.getUsageReport();
const warnings = [];
for (const m of report.metrics) {
if (m.limit <= 0) continue;
const percentage = (m.used / m.limit) * 100;
if (m.exceeded) {
warnings.push({ level: 'critical', metric: m.metric, message: `${m.metric} limit exceeded (${m.used}/${m.limit})` });
} else if (percentage >= thresholdPercent) {
warnings.push({ level: 'warning', metric: m.metric, message: `${m.metric} at ${percentage.toFixed(1)}% of limit` });
}
}
return warnings;
}
}
// Usage
const checker = new UsageStatusChecker(
process.env.LICENSE_KEY,
'https://your-license-api.com/v1'
);
const warnings = await checker.checkForWarnings();
warnings.forEach(w => console.warn(`[${w.level}] ${w.message}`));Best Practices
1. Batch high-frequency events
Don't call track-usage once per request for high-volume metrics — accumulate locally and flush periodically (see BatchedUsageTracker above). Every call is a real HTTP round-trip against the High Frequency rate-limit tier (1000 req/15min).
2. Handle the limit-exceeded case explicitly
A 403 Usage limit exceeded for metric means the increment was not applied — decide up front whether that should block the underlying action (hard enforcement) or just log a warning (soft enforcement); the server doesn't make that choice for you.
3. Poll usage-report, don't assume track-usage tells you everything
track-usage's response only reflects the one metric you just incremented. If you need to check status across all of a license's metrics (e.g. to show a usage dashboard), call usage-report instead.
4. Privacy and compliance
- Only track metrics you actually need for license enforcement
- Respect data retention policies for any historical data you store yourself (the server doesn't retain usage history beyond the current running total)
Next Steps
- Client Examples - Platform-specific tracking implementations
- Error Handling - Robust error management
- API Reference - Complete endpoint documentation
