Usage
The Usage API surfaces the same trace-count meter that powers the
in-product usage bars and the Stripe overage line items: a per-month
trace count read from DecisionTrace, weighed against the
organisation's plan limit, with month-over-month and projected-monthly
helpers attached. Tested against API version v1. JWT auth on every
endpoint — no API key path, no plan-gate: every paid
organisation can read its own counter, including the Sandbox tier
that gets hard-blocked at the limit. Read-only.
How the meter works
The counter is the DecisionTrace collection itself; there is no
secondary meter to drift away from. GET /current runs four
countDocuments() queries in parallel against the org's trace
collection (this-month / last-month / today / all-time) and computes
the derived fields in-process. Plan limits are read from
Organization.billing.plan:
| Plan | Monthly trace limit | Enforcement |
|---|---|---|
sandbox | 10,000 | Hard block (429 USAGE_LIMIT_EXCEEDED) |
scale | 100,000 | Soft — overage metered to Stripe |
governance | 500,000 | Soft — overage metered to Stripe |
enterprise | 2,000,000 | Soft — overage metered to Stripe |
custom | Unlimited | Counter still returned; unlimited: true |
A separate 60-second in-memory cache protects the trace-ingestion
hot path from countDocuments() pressure — the count read on
every POST /traces would otherwise dominate the p99 latency
budget. The dashboard read path on this resource is not cached
and always returns the live count, which is why a freshly-ingested
trace shows up here within milliseconds while the enforcement gate
on the same trace can lag by up to 60 seconds. This asymmetry is
deliberate: dashboards optimise for accuracy, the ingestion gate
optimises for the documented p95 < 25 ms budget on
POST /traces.
Endpoints
GET /api/v1/usage/current
GET /api/v1/usage/history
The legacy unversioned mount /api/usage returns the same payload
with a Deprecation header; migrate to /api/v1/usage.
Get current usage
GET /api/v1/usage/current
Returns the live month-to-date counter for the caller's organisation plus a small bundle of dashboard-friendly derived fields.
curl https://api.adjudon.com/api/v1/usage/current \
-H "Authorization: Bearer $ADJUDON_JWT"
import os, requests
r = requests.get(
"https://api.adjudon.com/api/v1/usage/current",
headers={"Authorization": f"Bearer {os.environ['ADJUDON_JWT']}"},
)
u = r.json()["data"]
print(f"{u['thisMonth']:,} / {u['limit']:,} ({u['percentUsed']}%)")
Response fields (200 OK):
| Field | Type | Description |
|---|---|---|
plan | enum | sandbox, scale, governance, enterprise, custom |
limit | number | null | Monthly trace ceiling; null for custom |
unlimited | boolean | true only for custom |
thisMonth | number | Trace count from the 1st of this month, server time (UTC) |
percentUsed | number | 0-100+; 0 for unlimited plans |
remaining | number | null | max(0, limit - thisMonth); null when unlimited |
resetDate | string (ISO 8601) | First of next month, server time |
today | number | Trace count from local midnight |
dailyAverage | number | round(thisMonth / dayOfMonth) |
projectedMonthly | number | round(dailyAverage * daysInMonth) |
lastMonth | number | Trace count for the prior calendar month |
monthOverMonthChange | number | Percent; 100 when lastMonth=0 and thisMonth>0; 0 when both zero |
totalAllTime | number | Lifetime trace count, no time filter |
status | enum | ok < 90% ≤ warning < 100% ≤ exceeded |
The status band is the same one the dashboard uses to colour the
usage bar and the same one the warning notification fires on at 90%
during ingestion. Errors: 401, 404 NOT_FOUND (org missing),
500 INTERNAL_ERROR.
Get usage history
GET /api/v1/usage/history
Returns the last twelve calendar months of trace counts — the exact array the dashboard's trend chart consumes. The current month is always the last element; partial-month counts are included as-is.
curl https://api.adjudon.com/api/v1/usage/history \
-H "Authorization: Bearer $ADJUDON_JWT"
Response (200 OK):
{
"success": true,
"data": [
{ "label": "Jun 2025", "count": 41203 },
{ "label": "Jul 2025", "count": 58719 },
{ "label": "Aug 2025", "count": 72104 },
{ "label": "Sep 2025", "count": 91386 },
{ "label": "Oct 2025", "count": 115720 },
{ "label": "Nov 2025", "count": 99814 },
{ "label": "Dec 2025", "count": 61905 },
{ "label": "Jan 2026", "count": 88240 },
{ "label": "Feb 2026", "count": 94111 },
{ "label": "Mar 2026", "count": 102877 },
{ "label": "Apr 2026", "count": 110345 },
{ "label": "May 2026", "count": 47208 }
]
}
Labels are server-formatted in en-US ("Mon YYYY"); render-side
locale conversion is the dashboard's responsibility. Errors: 401,
404, 500.
Common gotchas
- No API-key path. This is a JWT-only resource; the SDK does not
surface it because the SDK authenticates with
adj_live_*/adj_agent_*keys. Read it from a session-authenticated dashboard or a server-to-server JWT exchange. - Server time is UTC. Month boundaries are
new Date(y, m, 1)in the API server's local time, which is UTC on Fly.io Frankfurt. A trace ingested at 23:30 inEurope/Berlinon the last of the month may land in the next month on the meter; this matches the Stripe invoice cut-off. - No usage webhook today. Adjudon does not fire a
usage.thresholdevent. Page on the 90% threshold by configuring an Alert on the org-levelcpiScoreor by polling/usage/currenton a five-minute cron and reacting to thestatusfield. - The
unlimitedplan still returns numbers.thisMonth,today,dailyAverage,projectedMonthly,lastMonth,totalAllTimeare populated for every plan; onlylimit,remaining, andpercentUsedchange shape underunlimited. - Sandbox is the only hard block.
scale,governance,enterprisekeep ingesting after the limit and the overage flows through the Stripe metered-billing line item; the trace is never rejected. Thestatus: 'exceeded'flag is informational, not a rate-limit trigger, on those plans.
See also
- Billing API — the Stripe checkout, portal, and subscription state this counter feeds into
- Plans & Features — the feature-gate matrix and the overage-rate explainer
- Traces API — the source of truth this meter reads from
- Alerts API — how to page on the 90% threshold without a usage webhook
- Error Codes — the full error taxonomy