UsageBox Kata #1: From Token Event to Invoice Line in 30 Minutes

A hands-on kata: take a raw AI usage event - a chunk of Claude tokens, a tool call, a credit burn - and turn it into a stable, auditable invoice line using UsageBox, in about 30 minutes, without building a billing database. Six steps against the real metering API: send your first usage event; make retries safe (idempotent dedupe by event_id, with same-id-different-payload surfaced as a conflict); read a cheap month-to-date total from rollups; pull the immutable audit trail behind a disputed line with /explain; close the period to freeze the invoice while corrections land as net adjustments; and run a raw-vs-rollup /verify so the fast number always equals the true number. Plus production notes, kata variations (per-model cost, live spend caps, vendor-bill reconciliation, ad-hoc SQL), and what you just avoided building.

7 min read

usagebox katausage meteringusage-based billingidempotencyaudit trailrollupsperiod closeAI token billingmetering API2026

This is a hands-on kata, not a think-piece. The goal: take a raw AI usage event - a chunk of Claude tokens, a tool call, a credit burn - and turn it into a stable, auditable invoice line using UsageBox, in about 30 minutes, without standing up a billing database of your own. Every call below is a real endpoint on the live API, copied from the running service. By the end you will have idempotent ingest, a month-to-date total that is cheap to read, and the raw event trail behind every charge.

If you have read why a plain SQL usage_events table quietly breaks, this is the other half: what it looks like to just use a store that holds the billing invariants for you.

Before you start: three keys, not one

Every record you send is addressed with ingest keys you chose when you created the objects in the dashboard: a subscription (who is being billed), a product_item (what they bought), and a meter (what you are counting). Your API key goes in the x-api-key header. Take all four from your own account - GET /api/v1/subscriptions, /api/v1/meters and /api/v1/api_keys return exactly what you need - because keys copied out of documentation are the single most common reason a batch is accepted and meters nothing.

Step 1: send your first usage event

Usage goes in as records. Each one carries the three ingest keys, a numeric value, and an optional ISO timestamp (defaults to now). One request can carry up to 1,000 of them:

curl -X POST https://api.usagebox.com/api/v1/usage \
  -H "x-api-key: $USAGEBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '[{
    "subscription": "acme-prod",
    "product_item": "assistant-seats",
    "meter": "claude-tokens",
    "value": 18450,
    "timestamp": "2026-06-16T10:14:05Z"
  }]'

The response tells you what landed:

{
  "message": "Usage batch accepted",
  "request_id": "req_...",
  "records": 1,
  "processed": 1,
  "skipped": 0,
  "received_at": "2026-06-16T10:14:06.128Z"
}

Accepted batches are processed inline, so the record is in your rollups by the time the call returns. There is no queue to wait on.

Step 2: read processed, never just the status code

This is the step people skip and then lose a month of data to. Validation is all-or-nothing on shape: a malformed batch is rejected with a 422 and nothing is written. But a batch that is shaped correctly and carries an ingest key that does not exist on your account is accepted and silently meters nothing. That is why the response says so out loud:

{
  "message": "Usage batch accepted but NO records were recorded. See skipped_details.",
  "records": 2,
  "processed": 0,
  "skipped": 2,
  "skipped_details": [ ... ]
}

A 200 with processed: 0 is a failure wearing a success costume. Assert on processed in your collector's tests, and log skipped_details - it names the key that did not resolve. If you ever wonder why a customer's usage is missing, this is where the answer is.

Step 3: make retries safe (the part that actually matters)

Usage collectors retry. Networks drop acks. So the real test is what happens when the same batch arrives twice. Send an Idempotency-Key header carrying a key you derive from the batch itself:

curl -X POST https://api.usagebox.com/api/v1/usage \
  -H "x-api-key: $USAGEBOX_KEY" \
  -H "Idempotency-Key: collector-2026-06-16T10:14Z-acme" \
  -H "Content-Type: application/json" \
  -d '[{ "subscription": "acme-prod", "product_item": "assistant-seats", "meter": "claude-tokens", "value": 18450 }]'

Replay that exact request and the records already committed are not counted again. Replay the same key with a different body and you do not get a silent overwrite - you get a 409 Conflict, which almost always means a buggy collector reusing one key for different usage. It surfaces in your logs instead of in a billing dispute three weeks later.

If part of a batch fails to commit, the response carries retry_with_same_idempotency_key: true and a hint. Follow it literally: replaying with the same key retries only the records that did not land, while a fresh key would double-count the ones that did.

Step 4: the number that goes on the invoice

Send a few hundred more records through the month, then ask for the total. Rollups are computed as the records land, aggregated per calendar month and per meter, so the read is cheap and never contends with ingestion - no SUM() over millions of rows under a lock at billing time:

curl "https://api.usagebox.com/api/v1/usage/rollups?month=2026-06&meter=claude-tokens" \
  -H "x-api-key: $USAGEBOX_KEY"
{
  "month": "2026-06",
  "rollups": [{
    "month": "2026-06",
    "subscription_key": "acme-prod",
    "product_item_key": "assistant-seats",
    "meter_key": "claude-tokens",
    "meter_unit": "tokens",
    "aggregation": "sum",
    "value": 18412900,
    "last_record_at": "2026-06-30T23:51:02.004Z"
  }]
}

Omit month and you get the current UTC month, which is your live month-to-date figure. Add subscription to scope it to one customer. The aggregation field is the one the meter was defined with, so the number means what your meter says it means.

Step 5: the evidence behind a disputed line

A customer disputes the June charge. "Trust our total" is not an answer - you need to show what they actually did. The raw records are readable back, newest first:

curl "https://api.usagebox.com/api/v1/usage?subscription=acme-prod&limit=200" \
  -H "x-api-key: $USAGEBOX_KEY"

Each row comes back with the identifiers you sent it under - subscription_key, product_item_id, meter_id, the value and the timestamp - so the identifier you ingest with is the identifier you get back. limit defaults to 50 and caps at 200.

Production notes before you ship it

  • Assert on processed. Not the status code, not records. It is the only field that means "this was metered".
  • Derive the idempotency key from the batch (collector id plus time window plus customer), so a retry naturally reproduces it and a genuinely new batch cannot collide.
  • Use your own ingest keys. Read them from /api/v1/subscriptions and /api/v1/meters rather than typing them; a key that does not resolve is skipped, not rejected.
  • Batch, but not too hard. Up to 1,000 records per request, validated all-or-nothing, so one malformed row fails the batch rather than corrupting half of it.
  • No lock-in. Your raw records read back out through the same API. The trail is yours.

Kata FAQ

Do I have to run the database myself? No. UsageBox hosts the metering engine; you send records and read totals over HTTP. The durable store, the rollups and crash recovery are handled for you.

What happens if my collector sends a batch twice? With an Idempotency-Key, the second copy does not double-count. Without one, it is new usage - so send the header.

Why did my batch return 200 and change nothing? An ingest key in it did not resolve on your account. Read processed and skipped_details; the message says so explicitly.

Can I get my data out? Yes. Raw records read back through GET /api/v1/usage, rollups through /api/v1/usage/rollups.

What you just avoided building

In five steps you got idempotent ingest, a durable write path, cheap month-to-date totals per meter, and a readable raw trail behind every figure. Built in-house, that is a write-ahead log, crash recovery, dedupe-under-retry, and two aggregation paths you have to keep consistent - a real database project, not a weekend usage_events table. That is the same realization driving the 2026 metering acquisition wave: Stripe bought Metronome and Adyen bought Orb because metering is strategic infrastructure that is hard to get right and expensive to get wrong.

This is Kata #1 of four. Continue the series: Kata #2, live spend caps on real-time usage, Kata #3, reconcile a vendor bill against your meter, and Kata #4, per-customer per-model cost with dimensions.

Keep reading: idempotency and late events in depth, why metering needs its own database, and the usage-based billing guide.

Key Topics

  • usagebox kata
  • usage metering
  • usage-based billing
  • idempotency
  • audit trail
  • rollups
  • period close
  • AI token billing
  • metering API
  • 2026

Related Articles

Explore more articles on similar topics to deepen your understanding of usage-based billing.

Why Usage Metering Needs Its Own Database (and What a SQL Table Quietly Breaks)

Most usage-pricing writing is about reading the meter your vendor gives you. This is about the layer underneath: the dat...

9 min readRead more

UsageBox Kata #2: Live Spend Caps and Real-Time Usage

Catch and cap AI spend before the bill lands. A hands-on kata against the real metering API: read an account month-to-da...

7 min readRead more

Reconcile an AI Vendor Bill Against Your Usage Meter

A hands-on AI billing reconciliation pattern: compare your monthly meter rollups with the provider bill, localize gaps b...

8 min readRead more

Explore More Articles

Discover our complete collection of usage-based billing guides and implementation patterns.

View all articles