Skip to content

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

bash
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:

json
{
  "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:

StatusBodyCause
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

javascript
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:

javascript
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:

bash
curl -X POST https://your-license-api.com/v1/usage-report \
  -H "Content-Type: application/json" \
  -d '{"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28"}'

Response:

json
{
  "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.

javascript
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