{"id":122,"date":"2026-09-17T01:50:38","date_gmt":"2026-09-16T17:50:38","guid":{"rendered":"https:\/\/wp.qoraapi.com\/ai-usage-metering-billing\/"},"modified":"2026-09-20T03:53:40","modified_gmt":"2026-09-19T19:53:40","slug":"ai-usage-metering-billing","status":"publish","type":"post","link":"https:\/\/qoraapi.com\/blog\/ai-usage-metering-billing\/","title":{"rendered":"Metering and Billing AI Usage Per User: A Practical SaaS Guide"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Metering AI usage per user means recording the token counts returned by every model call, tagging each record with the user, feature, and organization that caused it, and enforcing quotas from that same ledger. Requests are the wrong unit: providers bill tokens \u2014 including cached and reasoning tokens \u2014 and if you bill requests, a single heavy user can erase your gross margin without ever tripping a limit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers the accounting model, the instrumentation, and the quota and pricing decisions. It is written for a team that already ships AI features and is now adding a billing line for them.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why per-user AI metering is hard<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If an AI call were a normal API call, you would count invocations, multiply by a unit price, and be done. Five properties of model inference break that model.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Cost varies by two orders of magnitude per request.<\/strong> A 200-token classification and a 30,000-token document summary are both &#8220;one request.&#8221; Metering on requests gives every user the same bill and gives your heaviest tenant a subsidy.<\/li>\n<li><strong>Usage arrives at the end \u2014 or not at all.<\/strong> With streaming, token counts typically appear only in the final chunk. If the client disconnects mid-stream, you have already been billed for generated tokens you never received and, unless you handle it, never recorded.<\/li>\n<li><strong>Cached and generated input are priced differently.<\/strong> Prompt caching splits your input tokens into a cached prefix and a fresh remainder, often at very different rates. A schema with a single <code>input_tokens<\/code> field cannot represent that split, so it cannot be billed or reconciled accurately.<\/li>\n<li><strong>Reasoning tokens are billed but invisible.<\/strong> Reasoning-capable models may generate thousands of thinking tokens that the user never sees, billed at the output rate. Without a separate counter, your per-message cost is unpredictable and your margin is a surprise.<\/li>\n<li><strong>One user action is not one API call.<\/strong> A single chat turn can trigger retrieval, a router model, a tool-calling loop, and a final synthesis pass \u2014 5 to 20 calls across several models. Retries and timeouts add more, and a timeout after 3,000 output tokens still costs money.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The practical test: if you cannot answer <em>&#8220;what did user X cost us last month, broken down by feature?&#8221;<\/em> with one query, you do not have metering \u2014 you have a provider invoice and a guess.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What to count: a token taxonomy<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A ledger that stores only <code>input_tokens<\/code> and <code>output_tokens<\/code> will be wrong within a quarter. Count these classes separately, because they behave differently on both sides of the transaction.<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-stripes\"><table class=\"has-fixed-layout\"><thead><tr><th>Token class<\/th><th>Where it appears<\/th><th>Provider bills it<\/th><th>Bill the user<\/th><th>The trap<\/th><\/tr><\/thead><tbody><tr><td>Input \/ prompt<\/td><td><code>usage.prompt_tokens<\/code><\/td><td>Yes, at the input rate<\/td><td>Yes<\/td><td>Includes full conversation history \u2014 long chats grow super-linearly, not linearly<\/td><\/tr><tr><td>Cached input<\/td><td><code>prompt_tokens_details.cached_tokens<\/code><\/td><td>Yes, at a discount<\/td><td>Yes, at your discounted rate<\/td><td>Billing cached tokens at the full input rate silently overcharges and inflates reported margin<\/td><\/tr><tr><td>Output \/ completion<\/td><td><code>usage.completion_tokens<\/code><\/td><td>Yes, typically 2\u20134\u00d7 the input rate<\/td><td>Yes, at the output rate<\/td><td>Output dominates chat cost; a short prompt with a 2,000-token answer is not cheap<\/td><\/tr><tr><td>Reasoning \/ thinking<\/td><td><code>completion_tokens_details.reasoning_tokens<\/code><\/td><td>Yes, as output<\/td><td>Yes<\/td><td>Never shown to the user, so nobody notices it in testing; cap it explicitly<\/td><\/tr><tr><td>Tool \/ function-call tokens<\/td><td>Folded into input + output per step<\/td><td>Yes<\/td><td>Yes<\/td><td>Meter per model call, not per user message, or agent features look 10\u00d7 cheaper than they are<\/td><\/tr><tr><td>Embedding tokens<\/td><td><code>usage.prompt_tokens<\/code> on the embeddings endpoint<\/td><td>Yes, input-only rate<\/td><td>Yes<\/td><td>Ingestion runs offline, so it never passes through your request middleware<\/td><\/tr><tr><td>Non-token units<\/td><td>Tiles, seconds, characters (images, audio)<\/td><td>Yes<\/td><td>Yes<\/td><td>Not tokens at all \u2014 keep a parallel unit column or the ledger cannot sum a mixed account<\/td><\/tr><tr><td>Retries and failed calls<\/td><td>Your own logs<\/td><td>Partly \u2014 output generated before a timeout is billed<\/td><td>No<\/td><td>Never charge a user for your retry; absorb it and alert on the retry rate instead<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Note that &#8220;provider bills it&#8221; and &#8220;bill the user&#8221; are different sets, and the gap is your risk. Retries and aborted streams are billed to you but should never reach a customer&#8217;s invoice. Cached tokens are billed to you at a discount and should be passed through at that same discount \u2014 pocketing the difference looks like margin until a customer reconciles your usage page against their own logs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Instrumentation: capture usage on every call<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There is exactly one reliable place to capture usage: the code path that receives the provider response. Do not reconstruct it downstream from logs, and do not estimate it with a tokenizer in production \u2014 tokenizers drift with new model families and cost CPU on the hot path. Read the <code>usage<\/code> object the provider returns, tag it, and append it to an event sink.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import time, uuid\nfrom contextvars import ContextVar\n\n# Set once per inbound request by your auth middleware. Never read user_id\n# from a request body \u2014 a client could otherwise bill someone else.\nattribution = ContextVar(\"attribution\")\n\ndef meter(response, *, model, feature, latency_ms, sink):\n    \"\"\"Extract usage from one provider response and emit one metering event.\"\"\"\n    usage = getattr(response, \"usage\", None)\n    if usage is None:                       # errors and some proxies omit usage\n        return None\n    ctx = attribution.get()\n    details = getattr(usage, \"prompt_tokens_details\", None) or {}\n    out_details = getattr(usage, \"completion_tokens_details\", None) or {}\n    event = {\n        \"event_id\": str(uuid.uuid4()),      # idempotency key: the sink must dedupe on this\n        \"request_id\": ctx[\"request_id\"],    # same id for every step of one user action\n        \"user_id\": ctx[\"user_id\"],\n        \"org_id\": ctx[\"org_id\"],\n        \"feature\": feature,                 # closed enum you own: \"chat\", \"summarize\", \"agent_step\"\n        \"model\": model,\n        \"input_tokens\": usage.prompt_tokens,\n        \"cached_input_tokens\": details.get(\"cached_tokens\", 0),\n        \"output_tokens\": usage.completion_tokens,\n        \"reasoning_tokens\": out_details.get(\"reasoning_tokens\", 0),\n        \"latency_ms\": latency_ms,\n        \"occurred_at\": time.time(),\n        # Raw counts only. No dollars here \u2014 see \"store tokens, price at read time\" below.\n    }\n    sink.write(event)                       # append-only; never mutate a past event\n    return event\n\ndef metered_call(user_id, org_id, feature, messages, model, client, sink, **kw):\n    token = attribution.set(\n        {\"request_id\": str(uuid.uuid4()), \"user_id\": user_id, \"org_id\": org_id}\n    )\n    started = time.monotonic()\n    try:\n        resp = client.chat.completions.create(model=model, messages=messages, **kw)\n        meter(resp, model=model, feature=feature, sink=sink,\n              latency_ms=int((time.monotonic() - started) * 1000))\n        return resp\n    finally:\n        attribution.reset(token)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Four details decide whether this holds up at scale:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Streaming needs an explicit flag.<\/strong> Send <code>stream_options={\"include_usage\": True}<\/code>; the final chunk carries the usage object with an empty <code>choices<\/code> array. Without it, streamed requests record zero tokens \u2014 the single most common metering bug.<\/li>\n<li><strong>Emit asynchronously.<\/strong> Write to a queue or buffer, never block the response path on the sink. Metering rows are tiny compared to request logs, so do not sample \u2014 sampling makes per-user invoices wrong precisely at the volume where you need them. The techniques in our <a href=\"https:\/\/qoraapi.com\/blog\/llm-observability\/\">LLM observability<\/a> guide apply here.<\/li>\n<li><strong>Deduplicate on <code>event_id<\/code>.<\/strong> Your own retry logic, a queue redelivery, or an at-least-once sink will duplicate events. A unique key plus an upsert is cheaper than reconciling a doubled invoice later.<\/li>\n<li><strong>Record failed calls too.<\/strong> An error event with zero tokens is still evidence \u2014 it tells you whether a user is hammering a broken feature or whether a provider is degrading.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Attribution: rolling usage up to user, feature, and org<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Attribution is a context-propagation problem, not a database problem. If the right dimensions are not stamped at the moment of the call, no amount of post-processing recovers them. Five rules make it work:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Resolve identity once, at the edge.<\/strong> Auth middleware sets <code>user_id<\/code> and <code>org_id<\/code> in a request-scoped context. Everything downstream reads it. Accepting a user id from a payload turns your metering into a spoofable API.<\/li>\n<li><strong>Make <code>feature<\/code> a closed enum you own.<\/strong> Use <code>chat<\/code>, <code>summarize<\/code>, <code>agent_step<\/code> \u2014 not model names and not free-text tags. Models change quarterly; features do not, and feature-level cost is the number that drives product decisions.<\/li>\n<li><strong>Propagate a parent request id through fan-out.<\/strong> An agent turn that makes 15 calls should produce 15 rows sharing one <code>request_id<\/code>. That gives you a billable unit for pricing and a trace for debugging without sacrificing granularity.<\/li>\n<li><strong>Decide how async work is attributed.<\/strong> A nightly re-index belongs to the organization, not to whichever user&#8217;s action queued it. Pick that rule once and apply it everywhere, or your per-user totals will double-count background work.<\/li>\n<li><strong>Store tokens, price at read time.<\/strong> Never denormalize a dollar amount into the event. Provider rates change, your markup changes, and cached-token discounts change \u2014 if the currency is baked into the row, you can never restate history without rewriting it.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Two roll-ups pay for the whole system on day one. <strong>Cost per user per day<\/strong> is an anomaly detector: the top ten users by spend will show you a runaway loop, a prompt that grows unbounded, or an abuse case before your provider invoice does. <strong>Cost per feature per day<\/strong> is a product signal \u2014 it tells you which feature is worth its inference bill and which one to route to a cheaper tier, which is the core move in our guide to <a href=\"https:\/\/qoraapi.com\/blog\/reduce-ai-api-costs\/\">reduce AI API costs<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quotas and throttling: enforce limits before the invoice<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The classic failure is checking the balance after the call returns. By then the money is spent. Because you cannot know the exact output token count in advance, quota enforcement needs a two-phase pattern: <strong>reserve, then settle<\/strong>. Before the call, reserve an estimate against the user&#8217;s remaining budget; after the response, write the actual usage event and release the difference. A simple, defensible reservation is <code>max_tokens \u00d7 your most expensive rate for that tier<\/code>. Over-reserving frustrates legitimate heavy users, under-reserving lets them overshoot by exactly one call \u2014 so err on the side of one call.<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-stripes\"><table class=\"has-fixed-layout\"><thead><tr><th>Policy<\/th><th>Enforced at<\/th><th>User experience<\/th><th>Use it when<\/th><\/tr><\/thead><tbody><tr><td>Hard monthly cap<\/td><td>Pre-flight reservation<\/td><td>Blocked until reset or upgrade<\/td><td>Prepaid credits and free tiers<\/td><\/tr><tr><td>Soft cap + alert<\/td><td>Async, on the ledger<\/td><td>Email or in-app banner, service continues<\/td><td>Enterprise accounts where a hard stop is worse than an overage<\/td><\/tr><tr><td>Requests per minute<\/td><td>Gateway \/ edge, per key<\/td><td>429 with <code>Retry-After<\/code><\/td><td>Protecting shared capacity from one runaway script<\/td><\/tr><tr><td>Token budget per request<\/td><td><code>max_tokens<\/code> on the call<\/td><td>Shorter answers, no error<\/td><td>Cheapest control you have \u2014 set it everywhere by default<\/td><\/tr><tr><td>Concurrency cap<\/td><td>Scheduler \/ semaphore<\/td><td>Queued work, slower responses<\/td><td>Batch and agent workloads that would starve interactive users<\/td><\/tr><tr><td>Prepaid credit balance<\/td><td>Pre-flight, from the ledger<\/td><td>Top-up prompt<\/td><td>Self-serve plans where you carry the payment risk<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">When a limit trips, return the right status. <strong>429<\/strong> means &#8220;you are going too fast, retry later&#8221; and must include <code>Retry-After<\/code> plus a machine-readable body naming the limit and its reset time. <strong>402<\/strong> means &#8220;you are out of credit, top up.&#8221; Conflating them means well-written clients either retry forever against an empty wallet or give up on a limit that clears in ten seconds. The retry semantics and backoff behavior are covered in our <a href=\"https:\/\/qoraapi.com\/blog\/ai-api-rate-limits-429-errors\/\">429 and rate-limit handling guide<\/a>. One more rule: compute quotas from the same event ledger as billing. Two sources of truth \u2014 a Redis counter for limits and a warehouse table for invoices \u2014 always drift, and the drift always shows up as a support ticket.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Billing models: seat, usage, credits, and hybrid<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There are only three shapes, and the choice is driven by how much usage varies between your customers \u2014 not by what your competitors publish.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Seat-only.<\/strong> Simplest to sell and forecast. Correct only when the spread between your p50 and p95 user is under about 2\u00d7. The moment one tenant runs a batch job, a flat seat price converts your best customer into your worst-margin customer.<\/li>\n<li><strong>Pure usage \/ credits.<\/strong> You define an internal credit unit and convert it to tokens at a published ratio. Margin is predictable, but the ratio must stay stable when your provider rates move \u2014 if a rate change visibly repriced credits, customers read it as a price increase, so absorb small changes and reprice deliberately.<\/li>\n<li><strong>Hybrid (the B2B default).<\/strong> A seat fee includes a committed token allowance, and usage beyond it bills at the usage rate. Size the allowance from real data, not from the sales conversation.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Three decision rules keep a hybrid plan from leaking margin. First, price the seat so the included allowance costs you at most 25\u201335% of the seat price at <em>p90<\/em> usage; anything higher and one power user holds your gross margin hostage. Second, treat prepaid credit breakage as margin only up to roughly a fifth of sold credits \u2014 beyond that, customers feel cheated rather than forgetful. Third, never refund tokens you have already paid a provider for; refund the credit instead, and let the usage ledger show why.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Finally, surface the number in the product. A per-user usage page showing tokens, cost, and consuming features removes more billing tickets than any email you can send, and it turns metering into a retention feature instead of a back-office cost.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A gateway that meters for you<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Everything above assumes you own the request path and can inspect every provider response. If your calls already flow through an AI API relay, most of the plumbing is a byproduct: <a href=\"https:\/\/qoraapi.com\/\" target=\"_blank\" rel=\"noopener\">qoraapi.com<\/a> meters usage per API key, so each key becomes a metering boundary you can map to a user, a team, or a tenant \u2014 with token counts recorded on the relay side rather than reconstructed in your application.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That moves a specific set of work off your plate: token extraction for every provider and model family, streaming usage capture, retry and error accounting, and per-key rate limiting. What stays with you is the part only you can define \u2014 which key belongs to which user, which feature made the call, and what your pricing policy is. The build reduces to two things: stamp a key per user or tenant, and write a metering event per call with your own attribution context. The broader architecture is covered in our <a href=\"https:\/\/qoraapi.com\/blog\/ai-api-gateway-guide\/\">AI API gateway guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Should I bill cached input tokens to the user?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes, but at the discounted rate you actually pay. Cached tokens are real work that your provider charges for, so excluding them understates usage; charging them at the full input rate overstates it. Track them in a separate column so the pass-through rate is explicit and auditable.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How accurate does per-user metering need to be?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Accurate enough to reconcile to your provider invoice within a small percentage. In practice that means using the provider&#8217;s own <code>usage<\/code> object rather than estimating, recording failures and retries, and deduplicating events. If your monthly total lands within one call of the invoice, the residual is your infrastructure cost, not a billing error.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need a tokenizer to count usage?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Every major provider returns exact counts in the response, including for streaming when you request usage explicitly. Keep a tokenizer only as a pre-flight estimator for quota reservations or prompt budgeting, and accept that it will be approximate.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What is the minimum viable metering schema?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">One append-only event table with: <code>event_id<\/code>, <code>occurred_at<\/code>, <code>request_id<\/code>, <code>user_id<\/code>, <code>org_id<\/code>, <code>feature<\/code>, <code>model<\/code>, and the token columns \u2014 input, cached input, output, reasoning. That is enough to produce per-user invoices, feature cost reports, and quota checks from a single source of truth. Add a parallel unit column when you start billing images or audio.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Per-user AI billing fails on the accounting model, not on the invoicing UI. Count tokens by class \u2014 including cached and reasoning tokens \u2014 capture usage from the provider response in middleware that stamps user, feature, and org, store raw counts and price them at read time, and enforce quotas with a reserve-then-settle check that returns 429 with <code>Retry-After<\/code>. Then choose a billing shape that survives your p90 user. Put a metering gateway in front of the providers and the hard half of that list stops being your code.<\/p>\n\n\n\n\n<h3 class=\"wp-block-heading\">Related reading<\/h3>\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/qoraapi.com\/blog\/reduce-ai-api-costs\/\">How to Reduce AI API Costs: A Practical Guide for Developers<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/ai-api-gateway-guide\/\">What Is an AI API Gateway? A Practical Guide for Developers<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/llm-observability\/\">LLM Observability: Monitoring AI API Usage, Latency and Cost<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/batch-ai-api-processing\/\">Batch AI APIs: Processing Millions of Requests Affordably<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/connect-cursor-cline-continue-custom-api-endpoint\/\">How to Connect Cursor, Cline and Continue to a Custom AI API Endpoint<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/load-testing-llm-apps\/\">Load Testing LLM Apps: Throughput, TTFT, and Concurrency<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/prompt-management-versioning\/\">Prompt Management and Versioning in Production<\/a><\/li><\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>Account for AI tokens per user: what to count, how to instrument every call, quota enforcement, and SaaS billing models that don&#8217;t leak margin.<\/p>\n","protected":false},"author":1,"featured_media":121,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[5,6,9,7],"class_list":["post-122","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-api","tag-ai-api","tag-api-gateway","tag-developer-tools","tag-developers"],"_links":{"self":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts\/122","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/comments?post=122"}],"version-history":[{"count":2,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts\/122\/revisions"}],"predecessor-version":[{"id":264,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts\/122\/revisions\/264"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/media\/121"}],"wp:attachment":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/media?parent=122"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/categories?post=122"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/tags?post=122"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}