Skip to content

Cost and FinOpsGuide

Reconciling estimated LLM spend with provider invoices

Your per-request cost figures are an estimate. The provider's cost API is closer to the invoice. This guide shows how to pull both into the same daily shape, compare them without fooling yourself, and name the cause of a gap.

By INFRO Editorial

Published

8 min read

Two columns of glowing amber rows linked across the middle, aligning line by line on a dark ground, representing estimated and reported LLM cost

In short

Reconcile once a day: sum your per-request cost estimates per provider per UTC day, read the same day from the provider's cost API (OpenAI GET /v1/organization/costs, Anthropic GET /v1/organizations/cost_report, both with an admin key), and divide reported by estimated. Ignore today and single days; a ratio that holds steady for ten days or more is an unmodelled rate, a step is a rate change, and extra reported tokens are traffic you did not route.

Key takeaways

  • Reconcile at the grain the provider reports: one provider, one UTC day. Never spread a reported total back over individual requests.
  • OpenAI's Costs API returns dollars as a number; Anthropic's Cost Report returns cents as a decimal string. Normalize before comparing.
  • Exclude today, and judge a gap by its direction over at least ten days. A single day is usually timing.
  • A steady ratio below one is a negotiated rate; a step change is a new term; a gap that tracks cache writes or long prompts is a premium the estimate did not model.
  • Tokens on the provider's usage report that your gateway never sent are traffic outside it — a key used directly, or batch jobs.

Almost every cost number you see for LLM traffic is computed, not measured: token counts from the provider's response, multiplied by a rate table. It is a good estimate when the rate table matches your contract and every request went through the system doing the multiplying. Reconciliation is how you find out whether both are true.

This guide is the mechanical version: which endpoints to call, what shape the answers come in, how to compare them without chasing noise, and how to turn a persistent gap into a single named cause. For why the gap exists at all, see Reconcile your AI bill: estimate vs. provider invoice.

What you are comparing

Two figures, for the same provider and the same UTC day:

  • Estimated cost. The sum of your per-request estimates for traffic sent to that provider's accounts. It comes from your gateway, your logs, or your own metering.
  • Reported cost. What the provider's own cost API says your organization spent. It is the closest thing to the invoice you can read by API, and it is organization-wide.

Reconcile at that grain and no finer. A provider reports totals per day, per line item or workspace; it does not report the cost of your request req_123. Dividing a day's reported total back over individual requests produces numbers that look precise and mean nothing. Keep each request at the cost it was estimated at, and keep the comparison at the level both sides can see.

Your gateway or logs

Estimate per request

Provider-reported token counts times the rate you expect to pay, summed per provider per UTC day.

Provider cost API

Reported cost per day

OpenAI /v1/organization/costs in dollars; Anthropic /v1/organizations/cost_report in cents as a decimal string.

Ratio and direction

reported / estimated per day, excluding today. Look at the mean and spread over at least ten days, not at one day.

Name one cause

A steady ratio is a rate; a step is a change; a gap that tracks cache or long prompts is an unmodelled premium; extra tokens are traffic you did not route.

Correct forward

Enter the rate, price the model or find the key. Never re-price requests already recorded.
One daily reconciliation run: two figures per provider per UTC day, a ratio, a named cause and a forward-only correction.

Reading OpenAI's Costs API

OpenAI's organization Costs API lives at GET /v1/organization/costs and needs an Admin key, created separately from the keys that serve traffic. It takes start_time in Unix seconds, returns daily buckets (bucket_width of 1d), and can group results by line_item or project_id. Each bucket carries start_time, end_time and results; each result has an amount object with a numeric value and a currency such as usd. Pages continue while the response has a next_page cursor, passed back as page1.

The companion Completions Usage API, GET /v1/organization/usage/completions, returns token counts — input_tokens, input_cached_tokens, output_tokens and num_model_requests — in buckets of a minute, an hour or a day, groupable by model, project or API key1. Cost tells you how much; usage tells you how many tokens, and which key sent them.

Reading Anthropic's Cost Report

Anthropic's Usage and Cost Admin API needs an Admin API key (sk-ant-admin01-…) or another admin credential; workspace API keys do not work2. The cost endpoint is GET /v1/organizations/cost_report, daily only, groupable by workspace_id and description. Grouping by description adds parsed fields per line: model, token_type (uncached input, cache read, five-minute and one-hour cache creation, output), service_tier (standard or batch), context_window and cost_type3.

Two details matter for reconciliation:

  • Units. amount is a decimal string in the lowest currency unit: "123.45" in USD is $1.233. Read it as dollars and every Anthropic figure is a hundred times too large.
  • Coverage. Priority Tier costs are not in the cost endpoint, and the programmatic endpoints are not available for Claude Platform on AWS2. An account using either will never reconcile from this endpoint alone.

Usage data typically appears within five minutes of a request completing; the usage endpoint supports buckets of 1m, 1h and 1d, groupable by API key, workspace, model and service tier2.

Pulling both into one shape

The code below reads each provider's reported cost into { "YYYY-MM-DD": Decimal dollars }. Use Decimal rather than floats; you will be summing many small figures and comparing ratios.

reported_cost.py
import datetime as dt
import os
from decimal import Decimal

import requests

UTC = dt.timezone.utc

def _unix(day: dt.date) -> int:
    return int(dt.datetime.combine(day, dt.time(), UTC).timestamp())

def openai_daily_cost(start: dt.date, end: dt.date) -> dict[str, Decimal]:
    """OpenAI: amount.value is a number of dollars."""
    url = "https://api.openai.com/v1/organization/costs"
    headers = {"Authorization": f"Bearer {os.environ['OPENAI_ADMIN_KEY']}"}
    params = {"start_time": _unix(start), "end_time": _unix(end), "bucket_width": "1d", "limit": 30}
    totals: dict[str, Decimal] = {}
    while True:
        resp = requests.get(url, headers=headers, params=params, timeout=30)
        resp.raise_for_status()
        body = resp.json()
        for bucket in body.get("data", []):
            day = dt.datetime.fromtimestamp(bucket["start_time"], UTC).date().isoformat()
            for result in bucket.get("results", []):
                value = Decimal(str(result["amount"]["value"]))
                totals[day] = totals.get(day, Decimal(0)) + value
        if not body.get("next_page"):
            return totals
        params["page"] = body["next_page"]

def anthropic_daily_cost(start: dt.date, end: dt.date) -> dict[str, Decimal]:
    """Anthropic: amount is a decimal string of cents."""
    url = "https://api.anthropic.com/v1/organizations/cost_report"
    headers = {
        "x-api-key": os.environ["ANTHROPIC_ADMIN_KEY"],
        "anthropic-version": "2023-06-01",
    }
    params = {
        "starting_at": f"{start.isoformat()}T00:00:00Z",
        "ending_at": f"{end.isoformat()}T00:00:00Z",
        "bucket_width": "1d",
        "limit": 31,
    }
    totals: dict[str, Decimal] = {}
    while True:
        resp = requests.get(url, headers=headers, params=params, timeout=30)
        resp.raise_for_status()
        body = resp.json()
        for bucket in body.get("data", []):
            day = bucket["starting_at"][:10]
            for result in bucket.get("results", []):
                dollars = Decimal(result["amount"]) / 100
                totals[day] = totals.get(day, Decimal(0)) + dollars
        if not body.get("has_more"):
            return totals
        params["page"] = body["next_page"]

Your estimate side must be summed on the same boundary: the UTC day in which the provider processed the request. If your logs are in local time, convert before grouping.

Comparing without chasing noise

Compute a daily ratio, reported over estimated, and treat it with suspicion until it has a history.

compare.py
from decimal import Decimal
from statistics import mean, pstdev

def ratios(reported: dict[str, Decimal], estimated: dict[str, Decimal], today: str):
    days = sorted(d for d in reported if d in estimated and d < today and estimated[d] > 0)
    return [(d, float(reported[d] / estimated[d])) for d in days]

def summarize(series: list[tuple[str, float]], min_days: int = 10):
    values = [r for _, r in series]
    if len(values) < min_days:
        return {"status": "insufficient_data", "days": len(values)}
    m = mean(values)
    cv = pstdev(values) / m if m else None
    steady = cv is not None and cv <= 0.05
    return {"days": len(values), "ratio_mean": round(m, 3), "ratio_cv": round(cv, 3), "steady": steady}

Three rules keep the comparison honest:

  1. Exclude today. A part-day compared with a whole one shows a large gap every morning.
  2. Ignore single days. A request at 23:59 UTC can land in one system's day and the other's next day. Over a week these cancel.
  3. Watch direction, then size. Five days leaning the same way is information; one large day is not. This is the same discipline the FinOps Foundation describes for anomalies generally: detect, then investigate and document the cause before acting4.

reported / estimated

  • Day 10.81
  • Day 20.84
  • Day 30.79
  • Day 40.82
  • Day 50.83
  • Day 60.80
  • Day 70.82
  • Day 80.81
  • Day 90.83
  • Day 100.82
Illustrative: ten days of reported over estimated cost for one provider. The ratio holds near 0.82 with little spread, which is the shape of a negotiated rate the estimate does not know about.

Naming the cause

A gap has a direction and a shape, and together they usually point at one cause.

CauseDirectionShape over timeEvidence to check
Day boundaryEitherAlternating, cancels over a weekRequests near 00:00 UTC
Negotiated rateReported below estimateSteady ratio for weeksRatio mean and spread per model
Rate changedEitherStep on one day, then flatContract or vendor price date
Cache writes, long context, tiersReported above estimateTracks cache or long-prompt shareItemised cost lines by token type
Traffic outside the gatewayReported above estimateTracks another system's loadUsage by API key the gateway never used
Unpriced modelsReported above estimateStarts when a new model is routedRequests with no cost
Common causes of a reconciliation gap, the direction each pushes reported cost relative to the estimate, and the evidence that confirms it.
  • A negotiated rate. Reported cost is a steady fraction of the estimate. Anthropic, for example, says volume pricing is negotiated case by case5. Infer the effective rate per model — reported cost for that model divided by its tokens, times one million — and enter it. See negotiated rates vs list price.
  • A rate change. The ratio steps to a new level on one day and stays there. Find the contract term or the vendor's price change with that date.
  • An unmodelled premium. Reported cost runs above the estimate in proportion to something the estimate ignores: cache writes, a long-context tier, a service tier. Anthropic's cost report itemises each of these by token_type, context_window and service_tier3, so group by description and compare line by line.
  • Traffic outside your system. The provider reports more tokens than you sent. Group the usage endpoint by API key: a key your gateway never used is somebody's script, a notebook, or a batch job.
  • Unpriced requests. Your estimate has requests with no cost because a model has no rate. The estimate is a lower bound, and the gap is partly a coverage problem, not a pricing one. Count those requests separately.

A reconciliation checklist

  • Admin credentials for each provider, separate from serving keys, stored as secrets and read-only in use.
  • Estimates summed per provider per UTC day, from provider-reported token counts.
  • Anthropic amounts divided by 100; OpenAI amounts read as dollars.
  • Today excluded; no conclusion from fewer than ten days.
  • Unpriced requests counted beside the estimate, never as zero.
  • Usage grouped by API key, so traffic you did not route is visible.
  • Every correction applied forward only, with the dates and evidence recorded.

Reconciliation in INFRO

INFRO estimates the cost of every request when it serves it, from your own rate for the model and provider if you entered one, otherwise the vendor's list rate, and records the source of that rate on the request. On the Business plan, cost reconciliation runs the comparison above once a day: it reads OpenAI's GET /v1/organization/costs or Anthropic's GET /v1/organizations/cost_report with a separate billing credential (an OpenAI Admin key or an Anthropic sk-ant-admin key), stores reported and estimated cost side by side, and excludes today.

A persistent gap becomes a finding with one cause, a confidence and its evidence. A negotiated-rate cause needs the ratio to hold within 5% coefficient of variation over at least 10 days, and a suggested rate needs at least 7 days and $20 of reported spend for that model. Applying a suggestion writes a rate for that model and provider kind, prospectively; nothing already recorded is re-priced. With usage access switched on, INFRO also reads token counts by provider key and can say which traffic did not go through it; without it, bypass detection reports "not checked", never "no bypass".

List open findings
curl https://api.infro.io/v1/rate-findings \
  -H "Authorization: Bearer $INFRO_API_KEY"

When the estimate for a provider leans the same way by more than 5% on at least five of the last seven days, INFRO also raises a cost.drift alert. Your provider's invoice remains the authority; both INFRO figures are for planning, attribution and checking, and INFRO platform fees are never part of either.

From the team that builds it

Where this shows up in INFRO

INFRO is a control plane in front of the provider accounts you already hold. The capabilities below are the parts of it this article touches, each with the documentation that defines its behaviour.

  • Provider cost reconciliation

    Private early access

    Daily comparison of estimated spend against the cost reported by OpenAI's and Anthropic's own cost APIs, using a separate admin credential, with the likely cause of a persistent gap and a suggested rate you can apply. Included on the Business plan.

    Read the documentation for Provider cost reconciliation
  • Negotiated provider rates

    Private early access

    Enter the rates your contracts actually carry, per model and optionally per provider, so estimated cost uses them before the vendor's list rate. Included from the Team plan.

    Read the documentation for Negotiated provider rates
  • Spend anomaly and drift alerts

    Private early access

    Each scope's usual day is learned from its own history; a day that departs from it, a budget forecast to be exceeded, cost estimates drifting from the provider's own figures, unpriced traffic and failing connections become findings in the console. Routed to email on every plan, and to Slack and signed webhooks from the Startup plan.

    Read the documentation for Spend anomaly and drift alerts
  • Usage and cost per request

    Private early access

    Every request records its model, latency, token or unit counts, and estimated provider cost, queryable by project and key.

    Read the documentation for Usage and cost per request

INFRO is in private early access. Provider usage is billed to you by your providers; INFRO charges a software subscription and takes 0% of model spend. See plans.

Sources

Numbered in order of first citation. Each was opened and checked against the claim on the date shown.

  1. 1.How to use the Usage API and Cost API to monitor your OpenAI usage (opens in a new tab) — OpenAI. Accessed Sep 29, 2026.
  2. 2.Usage and Cost API (opens in a new tab) — Anthropic. Accessed Sep 29, 2026.
  3. 3.Get Cost Report (API reference) (opens in a new tab) — Anthropic. Accessed Sep 29, 2026.
  4. 4.Anomaly Management capability (opens in a new tab) — FinOps Foundation. Accessed Sep 29, 2026.
  5. 5.Pricing (opens in a new tab) — Anthropic. Accessed Sep 29, 2026.

About this article

Written from OpenAI's usage and cost API cookbook and Anthropic's Usage and Cost API documentation and reference, opened on 2026-09-29, and from the reconciliation documentation and implementation of the INFRO gateway, which the authors build. The ten-day ratio series is illustrative, not customer data. The code was written for this article against the documented request and response shapes.

Last checked against its sources on . The next review is due by . No separate named reviewer is recorded for this article.

How articles are written, sourced and corrected is set out in the editorial standards.