Skip to main content
You run a platform whose customers consume LLMs through you, and you bill them for what they use. This cookbook wires that end to end on LangWatch: one virtual key per tenant, caps for the tenant and for each of their end users, signed spend events into your billing ledger, reconciliation, breach handling, and period close. Your product keeps zero metering code of its own.

The runnable reference: agent-billing-demo

A tiny open-source agent-platform SaaS implementing every step below: sign-up provisions a real tenant key and real caps, chat rides the gateway, and the meters on screen are driven by the signed events its own receiver ingests. The webhook receiver, ledger, reconciliation loop, and provisioning exist in both TypeScript and Python. Point your coding agent at that repo as the canonical integration example; every file is written to be copied from.

What you need

  • An Enterprise plan or license (webhooks and the billing APIs gate on it).
  • Two credentials, because the surfaces are deliberately separated. A gateway key for provisioning (virtualKeys:*, gatewayBudgets:*), and an organization API key for the billing and webhook surfaces (webhookEndpoints:manage, gatewaySpend:view), whose permissions only an organization-scoped role can grant. The curls below show which one each call takes. See RBAC.
  • A provider credential configured once at organization scope (Settings > Model Providers). Tenants never see it.
The vocabulary used throughout: a tenant is your customer (one virtual key each); an end user is a person inside a tenant (no provisioning, attributed per request); a spend event is the metering record; an envelope is how events arrive at your webhook receiver.

1. Provision a tenant

Signing up a customer is four calls from your backend: mint the key, attach a hard cap and a soft cap, add the per-end-user allowance. Store the key id, the secret (shown once), and the budget ids on your customer row.
Choices worth making deliberately:
  • manual windows for tenant caps put period close in your hands: the budget accrues until your billing cycle resets it (step 6). Use calendar windows (month, aligned UTC) instead if you bill on calendar months and want automatic resets. The two combine freely.
  • The hard and soft caps are two budgets, not one field: block at the real ceiling, warn below it. Crossing 80 percent of either emits gateway.budget.threshold_crossed; hitting a limit emits gateway.budget.breached. See Budgets.
  • The attributed-user template is one rule, “each distinct end user on this key: $25 per month”. Buckets appear lazily on first spend; there is nothing to create or delete per user. Non-uniform caps are additional templates on other anchors.

2. Register your webhook endpoint (once)

Your billing ledger is fed by webhook endpoints. Register your receiver once, org-wide, subscribed to the spend stream (and the budget signals if you want them):
The 201 response includes the signing secret, shown once. Give it to your receiver, then use the endpoint’s test fire to prove the path before real traffic.

3. The request path

Your application calls the gateway with the tenant’s key and two attribution fields:
That is the whole integration on the request side. The user field (or the x-langwatch-end-user-id header, which wins over the body) is what the per-user template enforces on and what end_user_id carries on every spend event. The metadata echo is how your billing joins events back to your own records without lookups.
With an attributed-user template active, a request that carries no end-user id is rejected with error.code = "end_user_required" rather than passing uncapped. A cap evadable by omitting a field is not a cap. Send the id from day one.

4. Receive, verify, ingest

Your receiver does three things, in order:
  1. Verify the signature over the exact raw request bytes with your endpoint secret, rejecting stale timestamps. Copy the verifier from the webhooks reference or from the demo repo (ts/src/verify-signature.ts, python/verify_signature.py).
  2. Dedup by envelope id and ingest. At-least-once delivery plus stable ids means an upsert keyed on id is enough; retries and replays become no-ops.
  3. Handle the settled pair: a gateway.request.settled event books the request with unknown cost and needs_reconciliation; if a gateway.request.completed later arrives for the same gateway_request_id, replace the settled row with it. Never sum the pair. Field-by-field payload docs: Billing & spend events.
Money discipline: store cost.nano_usd as an integer, sum integers, round once at invoice time.

5. Reconcile

Nightly, or at period close, reconcile in two grains against the ledger (details: reconciliation):
Matching checksums end it. On divergence, walk spend-events for that key and window by cursor and diff by gateway_request_id; the missing or duplicated ids are your fix list. For a delivery gap (receiver outage, endpoint auto-disabled), replay the gap window to your endpoint, honoring your downstream biller’s dedup window.

6. Breach handling and period close

When a cap is hit, the request is rejected with 402 and machine-readable meta:
budget_scope is the branch your product copy needs: attributed_user means “your allowance ran out” (show the end user an upgrade path), virtual_key means “your organization’s cap ran out” (route to the tenant admin). The same moment emits gateway.budget.breached to your endpoint, so your backend learns about it without polling. On warn budgets the request still passes and the response carries the X-LangWatch-Budget-Warning header instead. At period close, reset the tenant’s manual budgets:
Reset moves the period boundary and recomputes the next reset. It never mutates recorded spend: the ledger and every emitted event are immutable, so reconciliation is unaffected by resets. On calendar windows a mid-period reset truncates the running period and the next boundary stays calendar.

7. The kill switch

For non-payment or abuse, stop a tenant reversibly:
Requests on the key are rejected with the distinct virtual_key_disabled error (403) until you call /enable. Budgets, scopes, key material, and any running rotation grace stay intact, and the change propagates to the gateway in seconds through the change feed. Both transitions emit gateway.virtual_key.disabled and gateway.virtual_key.enabled events. Reserve /revoke for the terminal goodbye; see Virtual keys.

Rendering budget bars in your UI

The pair every tenant dashboard wants, current spend against the cap:
  • Per tenant: GET /api/gateway/v1/budgets?scope_type=virtual_key returns each budget with live spent_usd and limit_usd.
  • Per end user: GET /api/gateway/v1/end-users/:id/spend (organization key) returns the user’s rolling-window usage and every applicable template cap at its current-period spend, in one call. See per-end-user spend.

Where to go deeper