Usage
Package usage is what your org ran and what it cost, broken down per account.
Package usage is what your org ran and what it cost, broken down per account.
| Base URL | https://api.hanzo.ai |
| Operations | 5 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1313 · Usage — The Metered Record — Draft · read the specification →
/v1/usage is the categorized read over what an org has spent and consumed, and
the write path for the samples a linked provider account contributes. The
implementation is hanzoai/cloud apps/usage.
A metered act writes in two places, neither of them here: the money lands on the
org's commerce ledger and the per-request detail lands in the hanzo.cloud_usage
warehouse ledger the ai plane writes (HIP-0106). This capability owns one series
of its own — hanzo.account_usage, what the collector posts — and everything
else it answers is a read over ledgers other capabilities own.
Motivation
"What did this cost me" is asked of three different records: the prepaid balance that was drawn down, the inference detail that explains the draw, and the provider accounts an org linked itself. Answering it from any one of them is wrong — the balance knows the money but not the model, the warehouse knows the model but not the invoice, the linked accounts know neither.
Answering it by copying all three into a fourth store is worse. A copy is a second number that can disagree with the ledger it came from, in the one place a customer will notice and dispute. So this capability composes rather than copies, and the composition is the specification.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The addresses
Every route is under /v1/usage (manifest/apps.go:271, package doc
apps/usage/usage.go), all typed, none declared:
POST /v1/usage— record account-usage samples, the collector's write path onto thehanzo.account_usageseriesGET /v1/usage/samples— one provider account's own lane, as a time seriesGET /v1/usage/summary— the org's footprint roll-up, §3GET /v1/usage/analyticsandGET /v1/usage/analytics/access— the entitlement-gated per-org read, and whether the caller holds the entitlement
Liveness is the host's generic GET /v1/usage/health, not one of these: the
plugin does not declare OwnsHealth, so the host's route stands on a path none
of the five shadows.
Per-model and time-series detail is NOT served here — that is /v1/event/*
— and the wallet's own raw drain is billing's /v1/billing/usage. Three
addresses answer three questions, and a fourth spelling of any of them would be
a fourth number to reconcile.
§2 What a metered act records
The recorder's wire type is deliberately narrow: subject, namespace, an EXACT
decimal USD rather than a rounded cent, currency, model and provider
(hanzoai/cloud ai.go:23-30). It carries no prompt content and no PII, and
the per-request detail — tokens, latency, per-model breakdown — is the
warehouse's, not this record's.
It carries no idempotency ref, and that absence is load-bearing: the ref this struct once held was derived from client-posted fields, which handed the ledger's dedup key to the payer. A future ref MUST be minted server-side or not exist.
The debit itself is not this capability's. It lands through the metering client
HIP-0106 specifies — authorize before the handler, record after — onto the
balance commerce owns. This surface reads that ledger; it never writes it.
§3 The rollup
GET /v1/usage/summary composes three sources: spend by category off the
commerce ledger, LLM totals off hanzo.cloud_usage, and the linked-account
board off the series in §4.
Each source MUST degrade independently to honest zeros, with a marker saying which sources answered. A roll-up that hides a dead source behind a plausible total is the worst failure this surface has, because the number still looks like money and a customer will act on it. A zero that says it is a zero is a fact; an unmarked one is a lie with a decimal point.
§4 Tenant, store, price, emission, stage, upstream
The tenant is the validated principal, fail-closed: no principal is 401 (HIP-0026). The commerce subject is pinned server-side to that org and every warehouse query binds the org positionally rather than by interpolation, so there is no request shape that widens the scope of a read.
It owns one store: the hanzo.account_usage series
(apps/usage/datastore.go), which is what the collector's write path fills.
Everything else it answers is a read over ledgers other capabilities own,
reached over the plane.
Reading it is free, in those words: Price: cloud.Free
(plugin/usage/main.go:21) — a lens over spend must not itself spend. It
publishes no events on the bus, so a customer's webhooks (HIP-1310) receive
nothing from it, and it emits nothing to observability beyond the request span
every route gets. The stage is ga — the manifest row declares none, and absent
is ga (HIP-0139 §8). It derives from no OSS upstream.
§5 The leaderboard boundary
leaderboard is its own capability with its own specification (HIP-1242) and
its own store — the opt-in rows. It is not a view this capability serves.
Today its three routes still answer under /v1/usage/*, which is the one
usage line in cloud's openapi/misfiled.txt; it closes by leaderboard moving
to /v1/leaderboard, never by alias, and /v1/usage is unchanged by the move.
The two stay two because the store boundary already exists: leaderboard owns the
opt-in rows and reads a derived pre-aggregation of hanzo.cloud_usage, which is
the warehouse's, not this capability's series.
Rationale
The alternative is a usage store: materialize every debit and every request row here and serve reads from it. It makes the reads trivial and introduces the one defect this surface cannot carry — a number that disagrees with the ledger a customer is charged against. Composition costs the fan-out in §3 and keeps the ledgers single-owner.
The alternative to marking degraded sources is a single total. It is friendlier in the console and unfalsifiable in support, because nothing in the response distinguishes "you spent nothing" from "the warehouse was unreachable".
Security Considerations
The wrong implementation hands an attacker another org's spend and activity profile — which models, how much, when — from a single unbound query. That is commercially sensitive on its own and an operational map of the tenant besides.
The boundary is therefore not a filter applied to results but a bind applied to every statement: the org is never an input a caller supplies, the commerce subject is derived server-side, and each warehouse query carries the org positionally. A read that composes three sources has three places to get that wrong, which is exactly why the rule is stated once and applies to all three.
The record's narrowness is the other control. Prompt content never enters this plane, so no breadth of read here discloses what a customer asked a model.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | usage at its own prefix | 5 operations |
| CLI | hanzo usage … | 5 of 5 |
| SDK | UsageApi in every published client | 5 methods |
| MCP | tool usage on https://api.hanzo.ai/v1/mcp | 5 operations, 1 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/usage/samples, operation get_usage_samples:
hanzo usage samplesimport { Configuration, UsageApi } from 'hanzoai';
const api = new UsageApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getUsageSamples();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import UsageApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = UsageApi(client).get_usage_samples()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.UsageAPI.GetUsageSamples(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, usage_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = usage_api::get_usage_samples(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.UsageApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new UsageApi(client).getUsageSamples();curl https://api.hanzo.ai/v1/usage/samples \
-H "Authorization: Bearer $HANZO_API_KEY"The door reaches usage through the usage tool, which names its 5 operations with its own verbs — this one among them, under a name only the door declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "list_usage_analytics"
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/usage/analytics/access | Echoes a plan's resolved analytics entitlement so a dashboard can configure itself against the LIVE catalog instead of hardcoding tier numbers. |
GET /v1/usage/analytics | Is the entitlement-GATED per-provider breakdown of the caller org's LLM usage — the paid lens over the same warehouse ledger GET /v1/usage/summary… |
GET /v1/usage/samples | Is the PER-PROVIDER view: one connected account's own consumption of its own plan — "my plan is 47% through its 6h window, resets at 14:20". |
GET /v1/usage/summary | Answers GET /v1/usage/summary: the caller's own usage footprint over one window — the categorized spend roll-up from the commerce ledger, the org's… |
POST /v1/usage | Ingests a batch of account-usage samples — what a developer's OWN AI accounts have consumed of their OWN plans, metered from each provider's own… |
How is this guide?