Skip to main content
Every request through the AI Gateway produces one spend event: token quantities by class, rated cost, and attribution (the virtual key, the end user, your echoed metadata). Spend events are pushed to your systems through webhook endpoints and pulled back for reconciliation over REST. Together they are the metering feed for platforms that rebill their own customers. Three properties make the feed billing-grade:
  • Nothing is silently missing. A request whose confirmation never arrived still produces an event, typed settled, with its cost marked unknown rather than zero. The worst case is a recorded request with an unknown cost, never a missing request.
  • Everything is idempotent. Event ids are stable across retries and replays. Recorded spend is never mutated: budget resets move period boundaries, corrections arrive as new events, and the ledger is append-only.
  • Push and pull agree. The pull surface serves the same canonical envelopes the webhooks deliver, from the same ledger, so a checksum mismatch means something real.
The billing surface (webhook delivery and the pull APIs) is an Enterprise capability, gated by the same plan flag as webhook endpoints. Reads require gatewaySpend:view; replay requires gatewaySpend:manage. Both are organization-tier permissions carried by an organization API key.

The spend event

Spend events ride the standard envelope ({id, type, created, schema_version, data}). The data payload:
Field notes:
  • event_id is the envelope id: the gateway request id with a type suffix. Dedup on it. gateway_request_id is the plain join key across the settled and completed pair, and it is the same ULID your client received in the X-LangWatch-Gateway-Request-Id response header, so you can join request-side logs to billing events.
  • occurred_at is request time, never ingest or delivery time. Late events land in the right invoice period.
  • status is success or error on completed events. Failed provider calls are events too; error carries {class, http_status} so your billing decides whether to charge for them.
  • end_user_id is the attributed end user (see attribution). metadata is your caller echo, returned verbatim: this is how billing joins events to your own tenant and user records.
  • model_provider_id names the provider credential that actually served the request.

Money representation

Get your ingest right once:
  • Quantities are exact integers per token class: input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, reasoning_tokens.
  • Cost is cost.nano_usd, an int64 in billionths of a USD ($0.003808 arrives as 3808000), together with rate_version, the pricing catalog version used to rate it. total_usd is a display-friendly decimal string of the same amount.
  • Never parse floats for money. Sum the integers; round exactly once, at invoice time. Currency is USD, single and stated.

Settled events and supersession

A request is admitted when the gateway accepts it and confirmed when its outcome (tokens, cost, duration) arrives. If the confirmation never arrives inside the settlement window (30 minutes by default), the request is settled:
The rules:
  • Quantities, cost, and duration are null: unknown is not zero. Never book a settled event as $0.
  • A settled request never appears in the completed stream, so your billing can trust completed for money and treat settled as a work queue.
  • If the confirmation later arrives, a real gateway.request.completed for the same gateway_request_id follows and supersedes the settled event. Your consumer replaces the settled figure with the completed one. Replace, never sum. The type-suffixed event ids exist precisely so your dedup layer does not swallow the second event.

Attribution: what the request carries

Attribution is captured on the request path; nothing is provisioned per end user.
  • End-user id resolution precedence: the x-langwatch-end-user-id header, then x-litellm-end-user-id (a migration alias, accepted so LiteLLM-shaped clients need no change), then the OpenAI user body field. Headers win over the body.
  • Metadata echo: send x-langwatch-metadata (a JSON object, 4 KB cap). It returns verbatim as metadata on every spend event for that request.
  • Request id: every gateway response carries X-LangWatch-Gateway-Request-Id, the same ULID that keys the spend event.
One resolver feeds both metering and per-end-user budget enforcement, so billing and capping can never disagree about who a request was.
end_user_id is whatever you send. The gateway stores and echoes it verbatim, so if your application passes a raw email address in the OpenAI user field, that address is what lands in webhook payloads, ledger rows, and the reconciliation pull, and it stays there for the 13-month retention window.Pass a stable pseudonymous id instead, such as a hash of the account id, whenever you would rather not have personal data in those places. It has to be stable, because it is the identity budgets and rollups group by; a value that changes per session bills the same person as many.

Reconciliation: two grains, one ledger

The pull surface reads the spend ledger, not the webhook stream, so it includes settled rows and admitted (in-flight) rows the push deliberately types differently. Reconcile in two passes: checksums first, item diff only on divergence.

Fast path: aggregate checksums

group_by is virtual_key or end_user; from/to are unix milliseconds and required; limit caps rows (default 500, max 1000); project_id optionally narrows. Each row:
settled_count counts unpriced requests separately; settled rows are never included in the cost sums. Compare a closed period’s event_count and nano_usd against what your billing ingested. Matching checksums end the reconciliation; totals-only comparison is not enough on divergence, because one dropped and one duplicated request of similar cost can net to nearly zero.

Item diff: the cursor walk

  • from and to (unix milliseconds) are required, must be safe integers, and from <= to; the walk is a ranged read by contract.
  • Pages are stable under live writes: rows are served in insert order, so a row folded late is picked up by a later page of the same walk instead of being skipped. Offset pagination cannot promise that; cursors can.
  • Filters: virtual_key_id, end_user_id, project_id, model, and status (success, error, admitted, confirmed, failed, settled). limit defaults to 50, max 200.
  • Rows come back as the same canonical envelopes the webhooks deliver, plus next_cursor. Diff by gateway_request_id.
  • A garbled cursor is refused with 400 rather than silently restarting the walk.
  • In-flight requests appear with status=admitted and null quantities (typed gateway.request.admitted on this surface only; they are never pushed). Treat them as not yet billable.

The settled work queue

GET /api/gateway/v1/spend-events?status=settled&from=...&to=... lists requests recorded without a price. Each is bounded, visible, and resolvable; when a late confirmation supersedes one, it leaves this queue and the completed event carries the money.

Retention

Spend records are retained for a fixed 13 months, independent of your organization’s trace retention settings. A tenant retention policy shrunk to weeks does not touch billing records. The 13 months bound your reconciliation and replay window by contract.

Per-end-user spend

The read a rebilling platform polls at period close, or renders in its own UI next to the user’s allowance:
One call answers both halves of the question: what this user consumed over a window, and which caps that counts against.
  • The rollup (cost, request_count, usage) reads the spend ledger over a rolling window: window is day, week, or month (last 24 hours, 7 days, or 30 days, defaulting to month), and explicit from/to in unix milliseconds override it.
  • caps is every attributed-user template in your organization that applies to this user, each carrying its current budget period spend, boundary-aware, which is the figure enforcement uses. That period is the budget’s own, so it will not match the rolling rollup above unless the two happen to align; the rollup is for showing usage, caps is for showing headroom. An empty array means no template applies to that user.
  • virtual_key_id narrows both the rollup and the caps to one anchor key.
Served on the billing surface: organization API key, gatewaySpend:view.

Replay

Re-delivers the window’s spend envelopes to one endpoint through the normal delivery path: per-endpoint stream, retry ladder, delivery log, and the endpoint’s own event subscriptions. Envelope ids are unchanged, so your consumer’s event-id dedup decides what a redelivery means. Caps and caveats:
  • The window is capped at 7 days per call, and a window holding more than 10,000 envelopes is refused with a request to narrow it.
  • The response reports {endpoint_id, replay_id, replayed, window}; replay_id identifies this replay in delivery bookkeeping without touching envelope ids.
  • Downstream dedup windows are finite (Metronome documents 34 days, Stripe meters 24 hours and up). Replaying a range older than your billing system’s dedup window can double-bill on your side. For old ranges, prefer pull-and-diff over replay.
Requires gatewaySpend:manage. The endpoint must be an active endpoint of your organization.

The ledger UI

Settings > Gateway > Billing events (/settings/gateway/billing-events) renders the spend ledger: cursor-paged, filterable by virtual key, end user, model, status, and date range, with token classes and cost per row and drill-through to the trace. Delivery health lives one screen over, on each endpoint’s deliveries view under Settings > Webhooks, so the pair to keep open during an integration bring-up is this ledger beside your endpoint’s health strip: if the numbers here match your ledger and the lag there stays near zero, the pipeline works.

Operational notes

  • Failed requests are events. Decide explicitly whether your billing charges for provider errors; status and error give you the branch.
  • Blocked requests are not. A request rejected before dispatch (budget breach, missing end-user id, disabled key) produced no provider spend and no spend event. The rejection is visible on the budget events and in gateway metrics instead.
  • Settlement window: 30 minutes by default. Self-hosted deployments can tune it; see self-hosting webhooks and billing.
  • No content, ever. Spend events carry ids, counts, classes, and your metadata echo. Prompts and responses never enter the billing pipe.

See also