Skip to content

Cost and FinOpsGuide

Detect AI API usage that bypassed your gateway

Your gateway can only report the traffic it served. The provider reports all of it. Read both per API key and per day, and the difference names keys used somewhere else: a leak, a script, a team going direct. The endpoints, the fields, a working script and how to read the result.

By INFRO Editorial

Published

8 min read

Two columns of amber glass rows on a dark ground, compared side by side, with one row marked in red

In short

To detect AI API usage that bypassed your gateway, read the provider's usage grouped by API key (OpenAI GET /v1/organization/usage/completions with group_by=api_key_id; Anthropic GET /v1/organizations/usage_report/messages with group_by[]=api_key_id, both with an admin credential) and compare each key's daily tokens with what your gateway sent on that key. A key your gateway uses that shows more tokens than it sent is being used elsewhere; a key your gateway does not know is shadow usage or another system; both need an owner by the end of the day.

Key takeaways

  • Your gateway can only count the traffic it served; the provider counts all of it. Compare the two per API key and per UTC day.
  • OpenAI's completions usage endpoint groups by api_key_id and project_id; Anthropic's messages usage report groups by api_key_id, workspace_id and account_id. Both need an admin credential.
  • A key your gateway uses that shows more tokens than it sent is being used elsewhere: treat it as leaked until someone explains it.
  • A key with usage that your gateway does not know is shadow usage or another system; it needs an owner and an entry in your inventory.
  • Record each provider key's ID when you create it, or you cannot map usage to keys later.

How do you detect AI API usage that bypassed your gateway? Read the provider's own usage report grouped by API key, read what your gateway sent on each key, and compare them per UTC day. OpenAI and Anthropic both report token usage per API key through their admin APIs. Tokens on a key your gateway uses that your gateway never sent mean the key is being used somewhere else. Tokens on a key your gateway does not know mean another workload, shadow usage, or a leaked key nobody has told you about.

This works because of a basic asymmetry. A gateway can only count the requests it served; anything sent straight to the provider is invisible to it, however good its budgets and alerts. The provider sees every request on your account. Read the provider's report and the gaps become visible.

What bypass looks like

Traffic reaches your provider account without passing your gateway for ordinary reasons and for alarming ones:

  • A copy of a gateway's provider key in a notebook, a script, a .env file on a laptop or a CI variable, used for "just one test".
  • A team going direct because the gateway lacks a model or a parameter they need.
  • A third-party tool holding its own key on your account: a coding agent, an evaluation service, a BYOK platform.
  • Console and playground use by people with access to the provider console.
  • A stolen key. Someone who copied a key from a repository, a log or a breached tool calls the provider from their own infrastructure. This is the case that turns into an invoice.

The first four need an owner and a policy; finding shadow AI API usage covers that side. The last one needs revocation today. The comparison below finds all five; telling them apart is the work after it.

What you need before you start

  1. An admin credential per provider, held by the one system that runs the check. OpenAI's usage endpoints need an admin key1. Anthropic's need an Admin API key or another admin credential; workspace API keys do not work2. These are powerful keys; least privilege for provider admin keys covers how to hold them.
  2. The provider's ID for every key your gateway uses. OpenAI identifies keys as key_… and Anthropic as apikey_… in usage reports. Record the ID when you create a key, in your credential inventory. Without that mapping you can see usage per key but cannot say which keys are yours.
  3. Your gateway's tokens per provider key per UTC day. Most gateways record which connection served each request and the token counts the provider returned. Export them grouped by connection and day.

Reading OpenAI usage per key

The completions usage endpoint is GET https://api.openai.com/v1/organization/usage/completions. It takes start_time (Unix seconds, inclusive), an optional end_time (exclusive), bucket_width of 1m, 1h or 1d, and group_by with any combination of project_id, user_id, api_key_id, model, batch, service_tier and api_source. api_key_ids filters to particular keys. With bucket_width=1d, limit defaults to 7 buckets and goes up to 31; pages continue through next_page, passed back as page3.

Each result carries input_tokens (including cached and cache-write tokens), output_tokens, num_model_requests, and, when grouped by it, api_key_id3. Without a group_by, fields such as project_id and model come back as null1.

OpenAI: yesterday's tokens per API key
START=$(date -u -d "yesterday 00:00" +%s)
END=$(date -u -d "today 00:00" +%s)
curl -s "https://api.openai.com/v1/organization/usage/completions?start_time=$START&end_time=$END&bucket_width=1d&group_by=api_key_id&group_by=project_id" \
  -H "Authorization: Bearer $OPENAI_ADMIN_KEY" |
  jq -r '.data[].results[] | [.api_key_id, .project_id, .input_tokens, .output_tokens, .num_model_requests] | @tsv'

To name the keys, list each project's keys with GET /v1/organization/projects/{project_id}/api_keys. Each key has id, name, created_at, last_used_at and its owner. Pass owner_project_access=any: without it, the endpoint applies membership-based visibility rules "which may exclude some enabled keys"4.

The completions endpoint covers completions traffic only. OpenAI exposes separate usage endpoints for other kinds of traffic, such as embeddings and images, in the same reference3; run the same check on each kind you use.

Reading Anthropic usage per key

Anthropic's endpoint is GET https://api.anthropic.com/v1/organizations/usage_report/messages. It takes starting_at and ending_at (RFC 3339), bucket_width of 1d, 1h or 1m, and group_by[] with any of api_key_id, workspace_id, account_id, service_account_id, model, service_tier, context_window, inference_geo and speed. Daily buckets default to 7 and go up to 31; pages continue while has_more is true5.

Each result carries uncached_input_tokens, cache_read_input_tokens, cache_creation (split into ephemeral_5m_input_tokens and ephemeral_1h_input_tokens) and output_tokens, plus api_key_id when grouped by it5. Three details matter for this check:

  • Usage from the Console playground "is not associated with an API key, so api_key_id will be null" even when grouping by key2.
  • account_id is set for requests made with OAuth sign-in rather than an API key; group by it too, or that usage appears unattributed5.
  • Usage typically appears within five minutes of a request completing2.
Anthropic: yesterday's tokens per API key
START=$(date -u -d "yesterday 00:00" +%Y-%m-%dT%H:%M:%SZ)
END=$(date -u -d "today 00:00" +%Y-%m-%dT%H:%M:%SZ)
curl -s -G "https://api.anthropic.com/v1/organizations/usage_report/messages" \
  --data-urlencode "starting_at=$START" --data-urlencode "ending_at=$END" \
  --data-urlencode "bucket_width=1d" \
  --data-urlencode "group_by[]=api_key_id" --data-urlencode "group_by[]=workspace_id" \
  -H "anthropic-version: 2023-06-01" -H "x-api-key: $ANTHROPIC_ADMIN_KEY" |
  jq -r '.data[].results[] | [.api_key_id, .workspace_id,
    (.uncached_input_tokens + .cache_read_input_tokens
     + .cache_creation.ephemeral_5m_input_tokens + .cache_creation.ephemeral_1h_input_tokens),
    .output_tokens] | @tsv'

To name the keys, GET /v1/organizations/api_keys returns each key's id, name, status, created_at, created_by, expires_at, partial_key_hint and scope6. It has no last-used field, so the usage report grouped by key is also how you learn when an Anthropic key was last used.

A comparison script

The script below reads yesterday and the six days before it from both providers, reads your gateway's export, and classifies every key. It expects a CSV from your gateway with the columns provider,key_id,day,tokens, where key_id is the provider's ID for the key that served the request and tokens is input plus output tokens as the provider returned them.

bypass_check.py
import csv, datetime as dt, os, sys
from collections import defaultdict
import requests

UTC = dt.timezone.utc
today = dt.datetime.now(UTC).date()
start = today - dt.timedelta(days=7)           # seven complete days, today excluded

def openai_usage():
    url = "https://api.openai.com/v1/organization/usage/completions"
    headers = {"Authorization": f"Bearer {os.environ['OPENAI_ADMIN_KEY']}"}
    params = {"start_time": int(dt.datetime.combine(start, dt.time(), UTC).timestamp()),
              "end_time": int(dt.datetime.combine(today, dt.time(), UTC).timestamp()),
              "bucket_width": "1d", "group_by": ["api_key_id"], "limit": 31}
    out = defaultdict(int)
    while True:
        body = requests.get(url, headers=headers, params=params, timeout=30).json()
        for bucket in body.get("data", []):
            day = dt.datetime.fromtimestamp(bucket["start_time"], UTC).date().isoformat()
            for r in bucket.get("results", []):
                out[("openai", r.get("api_key_id"), day)] += r["input_tokens"] + r["output_tokens"]
        if not body.get("next_page"):
            return out
        params["page"] = body["next_page"]

def anthropic_usage():
    url = "https://api.anthropic.com/v1/organizations/usage_report/messages"
    headers = {"x-api-key": os.environ["ANTHROPIC_ADMIN_KEY"], "anthropic-version": "2023-06-01"}
    params = {"starting_at": f"{start}T00:00:00Z", "ending_at": f"{today}T00:00:00Z",
              "bucket_width": "1d", "group_by[]": ["api_key_id"], "limit": 31}
    out = defaultdict(int)
    while True:
        body = requests.get(url, headers=headers, params=params, timeout=30).json()
        for bucket in body.get("data", []):
            day = bucket["starting_at"][:10]
            for r in bucket.get("results", []):
                cc = r.get("cache_creation") or {}
                tokens = (r["uncached_input_tokens"] + r["cache_read_input_tokens"]
                          + cc.get("ephemeral_5m_input_tokens", 0) + cc.get("ephemeral_1h_input_tokens", 0)
                          + r["output_tokens"])
                out[("anthropic", r.get("api_key_id"), day)] += tokens
        if not body.get("has_more"):
            return out
        params["page"] = body["next_page"]

def gateway_usage(path):
    out = defaultdict(int)
    with open(path, newline="") as f:
        for row in csv.DictReader(f):
            out[(row["provider"], row["key_id"], row["day"])] += int(row["tokens"])
    return out

reported = {**openai_usage(), **anthropic_usage()}
sent = gateway_usage(sys.argv[1])
gateway_keys = {(p, k) for (p, k, _) in sent}

for (provider, key, day), tokens in sorted(reported.items(), key=lambda x: (x[0][0], str(x[0][1]), x[0][2])):
    if key is None:
        print(f"{day} {provider} UNATTRIBUTED  {tokens:>12,} tokens (console, playground or OAuth use)")
    elif (provider, key) not in gateway_keys:
        print(f"{day} {provider} UNKNOWN KEY   {key} {tokens:>12,} tokens: not a gateway key, find its owner")
    else:
        through = sent.get((provider, key, day), 0)
        extra = tokens - through
        if extra > max(10_000, 0.02 * tokens):  # illustrative tolerance
            print(f"{day} {provider} BYPASS        {key} {extra:>12,} tokens not sent by the gateway")

Run it daily with python bypass_check.py gateway_usage.csv. Every line it prints is a key that needs an explanation. The tolerance is illustrative: tune it to your traffic, and expect small gaps from requests that failed after the provider counted tokens, or from clocks that put a request on different sides of midnight.

Reading the result

FindingMost likely meaningFirst action
Bypass on a key the gateway usesThe gateway's provider key is also used somewhere else: a copy in a script, another service, or a stolen keyFind the source within the day; if nobody can, revoke the key at the provider and replace it in the gateway
Unknown key with usageA workload or tool with its own key; a forgotten prototype; possibly a leaked key nobody trackedFind the owner from the key's name, creator and project or workspace; add it to the inventory or revoke it
Unattributed usagePeople using the provider console or playground, or OAuth sign-insGroup by account_id or user_id to see who; decide whether that use is allowed
A key in your inventory marked revoked or idle shows usageRevocation did not happen, or happened on the wrong keyTreat as an incident: revoke at the provider now

Two habits make the findings faster to act on. Put each provider key in its own project or workspace, so a finding already names the workload. And when a bypass finding appears, switch to hourly buckets for that key: the hour the extra traffic started usually points at a deploy, a person or a leak.

Limits of this check

  • It is after the fact. The fastest it can be is the provider's reporting delay; for Anthropic that is typically five minutes2. It tells you a key is being misused; it does not stop it. Provider-side spend limits and revocation do that; see how to limit the damage from a stolen key.
  • It covers what the endpoints cover. OpenAI splits usage by kind of traffic across several endpoints3, and Anthropic says code execution appears in its cost endpoint but not in the usage endpoint2. Check each kind of traffic you use.
  • The admin credential is itself a target. Keep it in one system, used read-only, and in your inventory.
  • Other providers are not covered here. Google, Azure and AWS report usage through their own billing and monitoring tools. The same idea applies: find the most specific identity each one reports usage under, and compare it with what your gateway sent under that identity.

For the daily comparison of cost rather than tokens, see reconciling estimated LLM spend with provider invoices.

Bypass detection in INFRO

On the Business plan, INFRO's cost reconciliation reads OpenAI's and Anthropic's cost reports once a day with a separate billing credential. With usage access switched on for that credential (off by default), INFRO also reads token counts per day grouped by provider API key and model, matches the provider's key identifiers against the keys on your INFRO connections, and raises a finding when the provider reports more tokens than INFRO sent, listing the keys in the evidence. A key it cannot match is shown as unmatched. It reads no prompts or completions. Without usage access, bypass detection reports not checked, never "no bypass".

This check exists because INFRO's own controls, like any gateway's, govern only traffic that goes through INFRO. A provider key stolen from anywhere, including from a platform that stores customers' provider keys as INFRO does, can be used directly against the provider. Usage monitoring at the provider is how you find that out, and the provider's spend limits and revocation are how you stop it.

Questions readers ask

How do I see usage per API key in OpenAI?
Call GET https://api.openai.com/v1/organization/usage/completions with an admin key and group_by=api_key_id. Each result carries api_key_id, input_tokens, output_tokens and num_model_requests per time bucket (1m, 1h or 1d). Filter to particular keys with api_key_ids. Other kinds of traffic, such as embeddings and images, have their own usage endpoints.
How do I see usage per API key in Anthropic?
Call GET https://api.anthropic.com/v1/organizations/usage_report/messages with an Admin API key and group_by[]=api_key_id. Results carry uncached_input_tokens, cache_read_input_tokens, cache_creation and output_tokens per bucket. Console playground usage has a null api_key_id. Data typically appears within five minutes.
What does it mean if my provider reports more usage than my gateway sent?
On a key your gateway uses, it means some requests on that key did not go through the gateway: a copy of the key in a script or notebook, another service, or a leaked key. On a key your gateway does not use, it is another workload or shadow usage. Either way the key needs an owner and an explanation; if nobody can give one, revoke it at the provider.
Can this detect a stolen key in real time?
Not quite. Both providers report usage in buckets as small as a minute, and Anthropic says data typically appears within five minutes, so a check every hour or every few minutes is possible. A daily check is the usual cadence for reconciliation; a key that matters can be checked hourly.

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

    Available with a plan

    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
  • Usage and cost per request

    Available with a plan

    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
  • Provider connections

    Available with a plan

    Connect provider accounts you already hold. Each credential is encrypted when saved, scoped to your organization and never shown again; applications use INFRO gateway keys instead.

    Read the documentation for Provider connections
  • Spend anomaly and drift alerts

    Available with a plan

    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

INFRO accounts are self-serve. 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 Oct 21, 2026.
  2. 2.Usage and Cost API (opens in a new tab) — Anthropic. Accessed Oct 21, 2026.
  3. 3.Completions usage (Admin API reference) (opens in a new tab) — OpenAI. Accessed Oct 21, 2026.
  4. 4.List project API keys (Admin API reference) (opens in a new tab) — OpenAI. Accessed Oct 21, 2026.
  5. 5.Get Messages Usage Report (opens in a new tab) — Anthropic. Accessed Oct 21, 2026.
  6. 6.List API Keys (opens in a new tab) — Anthropic. Accessed Oct 21, 2026.

About this article

Endpoints, parameters and fields are taken from OpenAI's API reference for organization usage and project API keys, OpenAI's usage cookbook, and Anthropic's Usage and Cost API guide, usage report reference and API key reference, all opened on 2026-10-21. The script was written for this article against those documented shapes and is a minimal starting point, not production code. The thresholds are illustrative. Written by the team that builds INFRO, whose reconciliation performs this comparison on the Business plan; its behaviour is taken from INFRO's published documentation.

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.