The shape
- Each end-user has their own VK. One per customer, or per seat, your call.
- You set per-customer budgets. Enforced at the gateway. When a customer hits their cap, the gateway returns
402 budget_exceeded. Your code doesn’t need to track spend. - All audit + trace data lands in your LangWatch project. End-users never see LangWatch.
- End-users can’t escape your policy. Policy rules, model allowlists, cache rules all attach to the VK.
Step 1: Model the tenancy
Decide your scope granularity:
For this walkthrough, assume per-customer model. Note that
principal budgets target real LangWatch user accounts, which your external customers are not; for reseller tenancy the per-customer unit is the virtual key itself.
Step 2: Configure the upstream ModelProvider once
Your LangWatch org owns a single ModelProvider row per upstream (OpenAI, Anthropic) at ORGANIZATION scope. End-users never see these. Configure them under Settings → Model Providers → Add Model Provider (Scope = Organization), then open each row’s Advanced (Gateway) tab and set the per-credential gateway caps:
No separate “binding” entity to mint. The ModelProvider id is what your code references:
Step 3: Provision a VK with its cap when a customer signs up
One call mints the key AND its budget, atomically: the key can never exist for a moment without the cap you asked for. Server-side code (Node.js example, plain REST):user field (or the X-LangWatch-End-User-Id header); see Budgets → per-end-user budgets.
The eligible-provider set is the union of every ModelProvider visible from the key’s scopes (the ones you configured in Step 2). Pin routing_policy_id plus routing_mode: "policy" if you run a custom ordering; otherwise leave routing at its default.
In a signup webhook:
Step 4: Change the cap when the plan changes
Thebudget field on update upserts the key’s own cap, so plan changes are one call keyed by the VK id you stored (no budget id bookkeeping):
budget: null to remove the cap entirely (the spend history stays). Changes propagate to the gateway within 30 s via the /changes feed, no restart, no customer impact.
Step 5: End-user makes a call
Your customer’s app calls the gateway directly with the VK you gave them:- Authenticates the VK.
- Checks the customer’s budget.
- Applies your
modelsAllowedallowlist. - Dispatches to OpenAI using your provider credential.
- Meters the cost against the customer’s budget.
- Emits a LangWatch trace into your project, attributed to the key.
Step 6: Show the customer their spend and bill them
For display, the read-back is one REST call per key:spend_available: false in that output means spend could not be totalled server-side; hold billing until it recovers rather than invoicing a zero.
For invoicing, do not poll aggregates: ingest the per-request spend event stream through a signed webhook endpoint into your own billing ledger, and reconcile against spend-summaries checksums at period close. Every event carries exact integer quantities per token class, integer nano-USD cost, the tenant key, the end-user id, and your echoed metadata, which is what a rebilling invoice needs and what an aggregate cannot give you. The full billing loop (receive, verify, dedup, settled supersession, reconcile, period close) is its own cookbook: Metering and rebilling your customers, with a runnable reference implementation in TypeScript and Python.
Rating stays in your billing system: you define the markup over the gateway-reported cost (or bill passthrough plus a management fee), and events carry rate_version so cost is re-derivable.
Handling over-limit customers
Whenon_breach: block, the gateway returns 402 budget_exceeded. Your customer sees:
- Show an upgrade CTA in the end-user’s UI.
- Fall back to a free-tier response (“you’ve hit your monthly cap; upgrade for unlimited”).
error.type.
Suspension, rotation, revocation
Customer stops paying, or you need an emergency stop →POST /virtual-keys/:id/disable. Requests are rejected with the distinct 403 virtual_key_disabled until you call /enable; budgets, config, and any running rotation grace stay intact, and propagation is seconds. This is the reversible kill switch, use it for anything the customer might come back from. See Virtual Keys → Disable and enable.
Customer resets their API key in your UI → POST /virtual-keys/:id/rotate, get a new secret, send it to the customer. The old secret keeps authenticating for a 24 h grace window, so a client you have not reached yet does not start failing mid-rollout. The gateway’s cache TTL (~60 s) is only how long the new secret takes to be accepted everywhere, not when the old one expires.
Customer is gone for good → POST /virtual-keys/:id/revoke. The next request returns 403 virtual_key_revoked. One-way; for a cancellation grace period, disable now and revoke after the grace, rather than scheduling a bare revoke.
Audit: who did what
Every write through your backend credential is audited with action, target, and before/after metadata. Scoped API keys attribute the write to the key’s owning user; legacy project keys carry no user, so their writes record the machine principalsvc_<your_project_id>. Filter the audit log on target virtual_key to get a per-customer provisioning history, under /settings/audit-log in the LangWatch UI.
If you need SIEM export: query ONLY the gateway-shape AuditLog rows with a read-only role scoped to that table, and ship the filtered result set, never a whole-database dump (which would carry unrelated, sensitive application data). For example, a daily job running COPY (SELECT id, "organizationId", "userId", action, "targetKind", "targetId", "createdAt" FROM "AuditLog" WHERE "organizationId" = '<your-org-id>' AND "targetKind" IN ('virtual_key', 'budget', 'model_provider', 'routing_policy', 'cache_rule') AND "createdAt" >= now() - interval '1 day') TO STDOUT WITH CSV into your SIEM’s ingestion path: an explicit column list pinned to your organization, leaving the before/after payloads out unless your SIEM genuinely needs them. There is no public REST audit-export endpoint in v1, see Audit log → Querying programmatically for the supported paths (UI CSV download, direct SQL).
Gotchas
- Never let the customer see your backend API token. It has
virtualKeys:create; they’d provision more VKs charged to you. - Rotate VKs when an end-user leaves your customer’s org. Otherwise the ex-user keeps spend access until the budget resets.
- Name keys after your tenant id (
customer-<id>) at creation time. The key’s name and id are your join keys for spend read-back and audit history;principal_user_idis only for real LangWatch user accounts, not your external customers. - Test the
402path in your app before go-live. Many apps have unhandled exceptions on budget breach and crash the user’s flow. - Budgets scoped to
virtual_keyare the right level for per-customer enforcement, and the inlinebudgetfield on create/update manages exactly that budget for you.
Rate limits at your gateway level
If you want to throttle a specific customer without moving them to a different plan:429 rate_limit_exceeded. Combine with a short-window budget (hour, minute) for finer control.
See also
- Metering and rebilling your customers: the event-driven billing loop this provisioning pattern plugs into.
- Management REST API: every endpoint used above.
- Virtual Keys: VK lifecycle semantics, including disable vs revoke.
- Budgets: hierarchical scope logic and per-end-user templates.
- Security: how backend tokens are isolated from VK secrets.
- CI smoke-test cookbook: validate the full flow in CI.