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.
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.manualwindows 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:
blockat the real ceiling,warnbelow it. Crossing 80 percent of either emitsgateway.budget.threshold_crossed; hitting a limit emitsgateway.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):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: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.
4. Receive, verify, ingest
Your receiver does three things, in order:- 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). - Dedup by envelope
idand ingest. At-least-once delivery plus stable ids means an upsert keyed onidis enough; retries and replays become no-ops. - Handle the settled pair: a
gateway.request.settledevent books the request with unknown cost andneeds_reconciliation; if agateway.request.completedlater arrives for the samegateway_request_id, replace the settled row with it. Never sum the pair. Field-by-field payload docs: Billing & spend events.
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):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 with402 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:
7. The kill switch
For non-payment or abuse, stop a tenant reversibly: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_keyreturns each budget with livespent_usdandlimit_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
- Billing & spend events: every payload field, money rules, reconciliation, replay.
- Webhooks: signatures, retries, auto-disable, health, delivery controls.
- Budgets: scopes, windows including
manual, thresholds, the breach error catalog. - Multi-tenant SaaS reseller pattern: the provisioning-side cookbook this billing loop plugs into.
- The demo agent platform: what the reference repo shows and how to run it.