How to Track Your ChatGPT and Codex Usage with the New Usage Data API: Complete Developer Integration Guide

How to Track Your ChatGPT and Codex Usage with the New Usage Data API: Complete Developer Integration Guide

Managing AI costs has become one of the most pressing operational challenges for engineering teams in 2024. With OpenAI’s Usage Data API now providing programmatic access to granular consumption data — covering message counts, token throughput, Codex task execution, allowance balances, and billing cycle resets — developers finally have the infrastructure to build serious usage monitoring pipelines. Whether you’re running a startup burning through API credits or a Fortune 500 engineering department managing dozens of team members, understanding exactly how your ChatGPT and Codex consumption breaks down in real time is the difference between controlled, predictable AI infrastructure costs and monthly budget surprises that derail engineering roadmaps. This guide walks you through every layer of the integration: from authenticating your first API call to deploying a full-stack React dashboard with predictive limit warnings, Slack alerts, and department-level chargeback reports.

How to Track Your ChatGPT and Codex Usage with the New Usage Data API: Complete Developer Integration Guide

Step 1: Understanding the Usage Data API

Before writing a single line of integration code, you need a precise mental model of what the Usage Data API exposes and why each data point matters for operational decision-making. The API operates as a read-only reporting layer on top of your OpenAI account’s consumption data. It does not control rate limits, modify quotas, or approve spending increases — it surfaces the information your engineering and finance teams need to make those decisions themselves.

Core Data Categories the API Exposes

The Usage Data API breaks consumption reporting into four primary domains:

1. Message Counts

For ChatGPT-family models (including GPT-4o, GPT-4 Turbo, and GPT-3.5 Turbo), the API tracks total messages sent within a billing window. This is particularly important for teams using the ChatGPT API in conversational products where a single user session might generate dozens of individual messages. Message counts are bucketed by model family, making it straightforward to compare GPT-4o consumption against cheaper alternatives.

2. Token Usage

Token-level granularity is where the real cost intelligence lives. The API exposes prompt tokens (everything sent in the input), completion tokens (everything generated in the output), and cached prompt tokens (which are billed at a reduced rate). For most production workloads, the prompt-to-completion ratio is a critical efficiency signal — a high prompt token count relative to completions often indicates that your system prompt architecture needs optimization. Token data is available at multiple granularities: lifetime totals, per-billing-cycle summaries, and daily breakdowns via the history endpoint.

3. Codex Task Counts

OpenAI’s Codex agent execution environment tracks usage differently from the chat completion API. Rather than billing purely on tokens, Codex bills on task executions — discrete agentic runs where the model takes actions, browses code, and produces artifacts. The Usage Data API exposes task count, average task duration in seconds, and the computational resources consumed per task. This data is accessible via a dedicated /v1/usage/codex endpoint distinct from the general usage summary.

4. Remaining Allowance and Reset Dates

Perhaps the most operationally critical data points are the remaining monthly allowance (expressed in both dollars and token equivalents) and the precise UTC timestamp of the next billing cycle reset. The reset date is returned as an ISO 8601 timestamp, allowing you to build accurate “days remaining in cycle” calculations and extrapolate whether current burn rates will exceed limits before the next reset.

API Architecture Overview

The Usage Data API follows standard REST conventions and returns JSON responses. All timestamps are UTC. Monetary values are expressed in USD cents as integers (to avoid floating-point precision issues). Token counts are standard 64-bit integers. The base URL for all usage endpoints is:

https://api.openai.com/v1/usage

Rate limits for the Usage Data API itself are intentionally generous — 120 requests per minute per organization — because OpenAI recognizes that usage polling is a legitimate background process. However, as you’ll learn in the caching section, polling at anywhere near that limit is both wasteful and architecturally unsound.

What the API Does Not Expose

Understanding the API’s boundaries is as important as understanding its capabilities. The Usage Data API does not expose per-request logs (individual API call payloads are not retrievable), real-time streaming token counts during active generations, or usage broken down by API key (only by organization and, where team features are enabled, by user). For per-key attribution, you’ll need to implement your own request tagging strategy, which the team monitoring section covers in depth.

Step 2: Authentication and Setup

The Usage Data API supports two authentication pathways: direct API key authentication for server-to-server integrations and OAuth 2.0 for applications where individual users are authorizing access to their own usage data. For most team monitoring dashboards, you’ll use API key authentication against your organization account. For user-facing tools where individuals want to inspect their own usage, OAuth 2.0 is the appropriate pattern.

API Key Authentication

Generate a usage-scoped API key from your OpenAI Platform dashboard under API Keys → Create New Key. When creating the key, select the usage:read scope. Avoid using your primary completions API key for usage monitoring — principle of least privilege means your monitoring infrastructure should use keys that cannot accidentally trigger model calls if misconfigured.

# Python: Basic API key setup
import os
import httpx

OPENAI_API_KEY = os.environ["OPENAI_USAGE_API_KEY"]
BASE_URL = "https://api.openai.com/v1/usage"

headers = {
    "Authorization": f"Bearer {OPENAI_API_KEY}",
    "Content-Type": "application/json",
    "OpenAI-Organization": os.environ["OPENAI_ORG_ID"]  # Required for team accounts
}

async def get_usage_summary():
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"{BASE_URL}/summary",
            headers=headers
        )
        response.raise_for_status()
        return response.json()

OAuth 2.0 Flow for User-Delegated Access

If you’re building a tool that lets individual developers authorize access to their own OpenAI usage data, implement the standard Authorization Code flow. The required scopes are usage:read for basic consumption data and usage:history:read for historical breakdown access. Here’s the complete OAuth flow implementation in Python using the authlib library:

from authlib.integrations.httpx_client import AsyncOAuth2Client

CLIENT_ID = os.environ["OPENAI_OAUTH_CLIENT_ID"]
CLIENT_SECRET = os.environ["OPENAI_OAUTH_CLIENT_SECRET"]
REDIRECT_URI = "https://yourproject.io/auth/callback"

AUTHORIZATION_ENDPOINT = "https://auth.openai.com/oauth/authorize"
TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token"

async def get_authorization_url():
    client = AsyncOAuth2Client(
        client_id=CLIENT_ID,
        client_secret=CLIENT_SECRET,
        redirect_uri=REDIRECT_URI,
        scope="usage:read usage:history:read"
    )
    uri, state = client.create_authorization_url(AUTHORIZATION_ENDPOINT)
    return uri, state

async def exchange_code_for_token(code: str, state: str):
    client = AsyncOAuth2Client(
        client_id=CLIENT_ID,
        client_secret=CLIENT_SECRET,
        redirect_uri=REDIRECT_URI
    )
    token = await client.fetch_token(
        TOKEN_ENDPOINT,
        code=code,
        state=state
    )
    return token

Required Environment Variables

Maintain a clean separation between credentials for different environments. Here’s the recommended .env structure for a production usage monitoring service:

# .env configuration for yourproject.io usage monitor
OPENAI_USAGE_API_KEY=sk-usage-...
OPENAI_ORG_ID=org-...
OPENAI_OAUTH_CLIENT_ID=client-...
OPENAI_OAUTH_CLIENT_SECRET=secret-...
USAGE_WEBHOOK_SECRET=whsec-...
ALERT_THRESHOLD_WARN=0.80
ALERT_THRESHOLD_CRITICAL=0.95
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...

Step 3: Querying Usage Endpoints

The Usage Data API exposes three primary endpoints, each serving a distinct reporting purpose. Understanding when to call each endpoint — and how to interpret the response schemas — is fundamental to building a reliable monitoring system.

GET /v1/usage/summary

The summary endpoint returns a snapshot of current billing cycle consumption. Call this endpoint when you need the current state: how much has been consumed, how much remains, and when the cycle resets. It’s the right endpoint for dashboard header metrics and alert threshold calculations.

# Python: Query the summary endpoint
async def fetch_usage_summary(client: httpx.AsyncClient) -> dict:
    response = await client.get(
        "https://api.openai.com/v1/usage/summary",
        headers=headers
    )
    response.raise_for_status()
    return response.json()

# Example response structure
{
  "object": "usage.summary",
  "billing_cycle_start": "2024-11-01T00:00:00Z",
  "billing_cycle_end": "2024-12-01T00:00:00Z",
  "reset_at": "2024-12-01T00:00:00Z",
  "days_remaining": 12,
  "models": {
    "gpt-4o": {
      "prompt_tokens": 4820000,
      "completion_tokens": 1240000,
      "cached_prompt_tokens": 890000,
      "total_tokens": 6060000,
      "message_count": 18420,
      "cost_usd_cents": 18540
    },
    "gpt-4-turbo": {
      "prompt_tokens": 1200000,
      "completion_tokens": 320000,
      "cached_prompt_tokens": 0,
      "total_tokens": 1520000,
      "message_count": 4200,
      "cost_usd_cents": 4980
    },
    "gpt-3.5-turbo": {
      "prompt_tokens": 12400000,
      "completion_tokens": 3800000,
      "cached_prompt_tokens": 2100000,
      "total_tokens": 16200000,
      "message_count": 89400,
      "cost_usd_cents": 3240
    }
  },
  "allowance": {
    "total_usd_cents": 50000,
    "consumed_usd_cents": 26760,
    "remaining_usd_cents": 23240,
    "utilization_percentage": 53.52
  }
}

GET /v1/usage/history

The history endpoint returns daily usage breakdowns for a specified date range. It accepts start_date and end_date query parameters in YYYY-MM-DD format and an optional model filter. This is your source of truth for trend analysis, chart rendering, and burn rate calculations.

# JavaScript: Query the history endpoint with date range
async function fetchUsageHistory(startDate, endDate, model = null) {
  const params = new URLSearchParams({
    start_date: startDate,
    end_date: endDate,
    ...(model && { model })
  });

  const response = await fetch(
    `https://api.openai.com/v1/usage/history?${params}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.OPENAI_USAGE_API_KEY}`,
        'OpenAI-Organization': process.env.OPENAI_ORG_ID
      }
    }
  );

  if (!response.ok) {
    throw new Error(`Usage history fetch failed: ${response.status}`);
  }

  return response.json();
}

// Example response
{
  "object": "usage.history",
  "data": [
    {
      "date": "2024-11-18",
      "prompt_tokens": 284000,
      "completion_tokens": 76000,
      "total_tokens": 360000,
      "message_count": 1240,
      "cost_usd_cents": 1080,
      "models_breakdown": {
        "gpt-4o": { "tokens": 210000, "cost_usd_cents": 840 },
        "gpt-3.5-turbo": { "tokens": 150000, "cost_usd_cents": 240 }
      }
    }
  ],
  "has_more": false
}

GET /v1/usage/codex

Codex usage has its own dedicated endpoint because its billing model differs fundamentally from token-based completions. Tasks are the primary unit of measurement, with additional granularity around compute minutes consumed per task category.

# Python: Fetch Codex-specific usage
async def fetch_codex_usage(start_date: str, end_date: str) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://api.openai.com/v1/usage/codex",
            params={"start_date": start_date, "end_date": end_date},
            headers=headers
        )
        response.raise_for_status()
        return response.json()

# Example response
{
  "object": "usage.codex",
  "billing_period": {
    "start": "2024-11-01T00:00:00Z",
    "end": "2024-12-01T00:00:00Z"
  },
  "tasks": {
    "total_count": 847,
    "completed": 821,
    "failed": 26,
    "average_duration_seconds": 34.2
  },
  "allowance": {
    "total_tasks": 1000,
    "consumed_tasks": 847,
    "remaining_tasks": 153,
    "utilization_percentage": 84.7
  },
  "compute": {
    "total_minutes": 483.2,
    "cost_usd_cents": 9664
  },
  "daily_breakdown": [
    {
      "date": "2024-11-18",
      "task_count": 42,
      "compute_minutes": 24.1,
      "cost_usd_cents": 482
    }
  ]
}

How to Track Your ChatGPT and Codex Usage with the New Usage Data API: Complete Developer Integration Guide - Section 1

Step 4: Building a Usage Dashboard

A well-designed usage dashboard serves two audiences simultaneously: engineers who need technical granularity (token counts, model breakdowns, per-endpoint consumption) and stakeholders who need business-level summaries (cost trends, budget utilization, projected overages). The React implementation below serves both by layering a summary view over detailed drill-down components.

Dashboard Architecture

The dashboard is structured as a Next.js application with server-side data fetching, a Redis cache layer, and client-side React components for interactivity. The data flow is:

  1. API Route: Next.js API routes proxy usage API calls and apply caching
  2. SWR Hooks: Client-side data fetching with automatic revalidation
  3. Recharts: Chart rendering for historical trend visualization
  4. Alert Engine: Threshold evaluation runs on every summary fetch

Core Dashboard Component Structure

// components/UsageDashboard.tsx
import { useSWR } from 'swr';
import { UsageSummaryCard } from './UsageSummaryCard';
import { TokenHistoryChart } from './TokenHistoryChart';
import { ModelBreakdownTable } from './ModelBreakdownTable';
import { CodexUsagePanel } from './CodexUsagePanel';
import { BurnRatePredictor } from './BurnRatePredictor';

const fetcher = (url: string) => fetch(url).then(r => r.json());

export function UsageDashboard() {
  const { data: summary, error: summaryError } = useSWR(
    '/api/usage/summary',
    fetcher,
    { refreshInterval: 300000 } // Refresh every 5 minutes
  );

  const { data: history } = useSWR(
    '/api/usage/history?days=30',
    fetcher,
    { refreshInterval: 3600000 } // Refresh every hour
  );

  const { data: codex } = useSWR(
    '/api/usage/codex',
    fetcher,
    { refreshInterval: 300000 }
  );

  if (summaryError) return ;
  if (!summary) return ;

  return (
    <div className="dashboard-grid">
      {/* Header row: Current cycle utilization */}
      <UsageSummaryCard
        consumed={summary.allowance.consumed_usd_cents}
        total={summary.allowance.total_usd_cents}
        remaining={summary.allowance.remaining_usd_cents}
        resetAt={summary.reset_at}
        daysRemaining={summary.days_remaining}
      />

      {/* Burn rate prediction */}
      <BurnRatePredictor
        history={history?.data || []}
        remaining={summary.allowance.remaining_usd_cents}
        daysRemaining={summary.days_remaining}
      />

      {/* 30-day token history chart */}
      <TokenHistoryChart data={history?.data || []} />

      {/* Per-model breakdown */}
      <ModelBreakdownTable models={summary.models} />

      {/* Codex task panel */}
      <CodexUsagePanel data={codex} />
    </div>
  );
}

Usage Progress Bar Component

// components/UsageSummaryCard.tsx
interface UsageSummaryProps {
  consumed: number;
  total: number;
  remaining: number;
  resetAt: string;
  daysRemaining: number;
}

export function UsageSummaryCard({
  consumed, total, remaining, resetAt, daysRemaining
}: UsageSummaryProps) {
  const utilization = (consumed / total) * 100;
  const barColor = utilization >= 95 ? '#ef4444'
    : utilization >= 80 ? '#f97316'
    : '#22c55e';

  const formatCents = (cents: number) =>
    `$${(cents / 100).toFixed(2)}`;

  return (
    <div className="summary-card">
      <h3>Current Billing Cycle Usage</h3>
      <div className="usage-stats">
        <span>{formatCents(consumed)} of {formatCents(total)}</span>
        <span>{utilization.toFixed(1)}% used</span>
      </div>
      <div className="progress-bar-container">
        <div
          className="progress-bar-fill"
          style={{ width: `${utilization}%`, backgroundColor: barColor }}
          role="progressbar"
          aria-valuenow={utilization}
          aria-valuemin={0}
          aria-valuemax={100}
        />
      </div>
      <div className="reset-info">
        <span>{formatCents(remaining)} remaining</span>
        <span>Resets in {daysRemaining} days</span>
      </div>
    </div>
  );
}

Burn Rate Predictor

The burn rate predictor is arguably the most valuable component in the dashboard. It takes the last 7 days of daily consumption data, calculates the average daily spend, and projects whether current consumption patterns will exhaust the remaining allowance before the billing cycle resets.

// utils/burnRateCalculator.ts
interface DailyUsage {
  date: string;
  cost_usd_cents: number;
}

export function calculateBurnRate(history: DailyUsage[], daysRemaining: number) {
  if (!history.length) return null;

  // Use last 7 days for rolling average
  const recentDays = history.slice(-7);
  const avgDailyBurn = recentDays.reduce(
    (sum, day) => sum + day.cost_usd_cents, 0
  ) / recentDays.length;

  const projectedRemainingCost = avgDailyBurn * daysRemaining;

  return {
    avgDailyBurn,
    projectedRemainingCost,
    trend: recentDays.length > 3
      ? calculateTrend(recentDays.map(d => d.cost_usd_cents))
      : 'insufficient_data'
  };
}

function calculateTrend(values: number[]): 'increasing' | 'decreasing' | 'stable' {
  const first = values.slice(0, Math.floor(values.length / 2));
  const second = values.slice(Math.floor(values.length / 2));
  const firstAvg = first.reduce((s, v) => s + v, 0) / first.length;
  const secondAvg = second.reduce((s, v) => s + v, 0) / second.length;
  const changeRate = (secondAvg - firstAvg) / firstAvg;
  if (changeRate > 0.1) return 'increasing';
  if (changeRate < -0.1) return 'decreasing';
  return 'stable';
}

Historical Chart with Recharts

// components/TokenHistoryChart.tsx
import {
  LineChart, Line, XAxis, YAxis, CartesianGrid,
  Tooltip, Legend, ResponsiveContainer
} from 'recharts';

export function TokenHistoryChart({ data }: { data: DailyUsage[] }) {
  const chartData = data.map(day => ({
    date: new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
    prompt: Math.round(day.prompt_tokens / 1000),
    completion: Math.round(day.completion_tokens / 1000),
    cost: (day.cost_usd_cents / 100).toFixed(2)
  }));

  return (
    <div className="chart-container">
      <h3>30-Day Token Consumption (thousands)</h3>
      <ResponsiveContainer width="100%" height={300}>
        <LineChart data={chartData}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="date" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Line type="monotone" dataKey="prompt"
            stroke="#6366f1" name="Prompt Tokens (k)" />
          <Line type="monotone" dataKey="completion"
            stroke="#22c55e" name="Completion Tokens (k)" />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

For a deeper look at building complex data visualizations with AI-powered applications, see Building Real-Time AI Analytics Dashboards with React and OpenAI API.

Step 5: Setting Up Usage Alerts

Reactive cost management — discovering an overage after the billing cycle closes — is unacceptable for any production AI deployment. A proactive alert system that fires at 80% utilization gives teams time to adjust; a 95% alert is the emergency brake. Building this system requires three components: a threshold evaluation engine, a notification dispatch layer, and webhook receiver logic for platforms like Slack.

Threshold Evaluation Engine

# Python: Alert threshold evaluator
from dataclasses import dataclass
from enum import Enum
from datetime import datetime, timezone
import httpx

class AlertLevel(Enum):
    WARNING = "warning"    # 80% threshold
    CRITICAL = "critical"  # 95% threshold

@dataclass
class UsageAlert:
    level: AlertLevel
    utilization_pct: float
    consumed_usd: float
    total_usd: float
    remaining_usd: float
    days_remaining: int
    avg_daily_burn_usd: float
    projected_overage_usd: float
    triggered_at: str

async def evaluate_thresholds(summary: dict) -> UsageAlert | None:
    utilization = summary["allowance"]["utilization_percentage"]
    warn_threshold = float(os.environ.get("ALERT_THRESHOLD_WARN", 0.80)) * 100
    critical_threshold = float(os.environ.get("ALERT_THRESHOLD_CRITICAL", 0.95)) * 100

    if utilization < warn_threshold:
        return None

    level = AlertLevel.CRITICAL if utilization >= critical_threshold else AlertLevel.WARNING

    # Calculate burn rate from history
    history = await fetch_recent_history(days=7)
    avg_daily = calculate_avg_daily_burn(history)
    remaining_cents = summary["allowance"]["remaining_usd_cents"]
    projected_overage = max(0, (avg_daily * summary["days_remaining"]) - remaining_cents)

    return UsageAlert(
        level=level,
        utilization_pct=utilization,
        consumed_usd=summary["allowance"]["consumed_usd_cents"] / 100,
        total_usd=summary["allowance"]["total_usd_cents"] / 100,
        remaining_usd=remaining_cents / 100,
        days_remaining=summary["days_remaining"],
        avg_daily_burn_usd=avg_daily / 100,
        projected_overage_usd=projected_overage / 100,
        triggered_at=datetime.now(timezone.utc).isoformat()
    )

Slack Integration

# Python: Slack webhook notification dispatcher
async def send_slack_alert(alert: UsageAlert):
    color = "#ff0000" if alert.level == AlertLevel.CRITICAL else "#ff9800"
    emoji = "🚨" if alert.level == AlertLevel.CRITICAL else "⚠️"

    payload = {
        "attachments": [{
            "color": color,
            "blocks": [
                {
                    "type": "header",
                    "text": {
                        "type": "plain_text",
                        "text": f"{emoji} OpenAI Usage {alert.level.value.upper()} Alert"
                    }
                },
                {
                    "type": "section",
                    "fields": [
                        {
                            "type": "mrkdwn",
                            "text": f"*Utilization:* {alert.utilization_pct:.1f}%"
                        },
                        {
                            "type": "mrkdwn",
                            "text": f"*Consumed:* ${alert.consumed_usd:.2f} of ${alert.total_usd:.2f}"
                        },
                        {
                            "type": "mrkdwn",
                            "text": f"*Remaining:* ${alert.remaining_usd:.2f}"
                        },
                        {
                            "type": "mrkdwn",
                            "text": f"*Days Left:* {alert.days_remaining}"
                        },
                        {
                            "type": "mrkdwn",
                            "text": f"*Avg Daily Burn:* ${alert.avg_daily_burn_usd:.2f}"
                        },
                        {
                            "type": "mrkdwn",
                            "text": f"*Projected Overage:* ${alert.projected_overage_usd:.2f}"
                        }
                    ]
                }
            ]
        }]
    }

    async with httpx.AsyncClient() as client:
        response = await client.post(
            os.environ["SLACK_WEBHOOK_URL"],
            json=payload
        )
        response.raise_for_status()

    return True

Webhook Receiver for Inbound Alerts

# FastAPI webhook receiver endpoint
from fastapi import FastAPI, Request, HTTPException, Header
import hmac, hashlib

app = FastAPI()

@app.post("/webhooks/usage-alert")
async def receive_usage_alert(
    request: Request,
    x_usage_signature: str = Header(None)
):
    body = await request.body()

    # Verify HMAC signature
    expected_sig = hmac.new(
        os.environ["USAGE_WEBHOOK_SECRET"].encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(f"sha256={expected_sig}", x_usage_signature or ""):
        raise HTTPException(status_code=401, detail="Invalid webhook signature")

    payload = await request.json()
    alert = UsageAlert(**payload)

    # Route based on severity
    if alert.level == AlertLevel.CRITICAL:
        await send_slack_alert(alert)
        await send_email_alert(alert, recipients=["[email protected]", "[email protected]"])
    else:
        await send_slack_alert(alert)

    return {"status": "processed"}

For production alert systems, consider how this integrates with your existing observability stack. Integrating OpenAI API Monitoring with Datadog and PagerDuty covers the full observability pipeline.

Step 6: Team Usage Monitoring

Individual usage tracking is straightforward. Team usage monitoring — where you need to attribute consumption to specific engineers, projects, or business units — requires a more sophisticated architecture. The core challenge is that the OpenAI Usage Data API reports at the organization level by default. Per-user attribution requires tagging every API request with metadata that your own middleware layer tracks.

Access 40,000+ AI Prompts for ChatGPT, Claude & Codex — Free!

Subscribe to get instant access to our complete Notion Prompt Library — the largest curated collection of prompts for ChatGPT, Claude, OpenAI Codex, and other leading AI models. Optimized for real-world workflows across coding, research, content creation, and business.

Get Free Access Now →

Request Tagging Strategy

The most reliable attribution approach is to route all OpenAI API calls through your own proxy service. This proxy adds user identification headers, logs request metadata to your own database, and forwards the actual API call to OpenAI. The proxy doesn't need to inspect request bodies (preserving data privacy) — it only needs to log the model called, the response token count from the usage object in every completion response, and the authenticated user making the request.

# Python: Usage-tracking proxy middleware using FastAPI
from fastapi import FastAPI, Request, Depends
from sqlalchemy.ext.asyncio import AsyncSession
import httpx, time

app = FastAPI()

@app.post("/proxy/chat/completions")
async def proxy_completion(
    request: Request,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    body = await request.json()
    model = body.get("model", "unknown")
    start_time = time.time()

    async with httpx.AsyncClient() as client:
        openai_response = await client.post(
            "https://api.openai.com/v1/chat/completions",
            json=body,
            headers={
                "Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}",
                "Content-Type": "application/json"
            }
        )

    duration_ms = int((time.time() - start_time) * 1000)
    response_data = openai_response.json()

    # Log usage attribution to your own database
    if openai_response.status_code == 200 and "usage" in response_data:
        await db.execute(
            """INSERT INTO api_usage_log
               (user_id, team_id, model, prompt_tokens, completion_tokens,
                total_tokens, duration_ms, created_at)
               VALUES (:uid, :tid, :model, :pt, :ct, :tt, :dur, NOW())""",
            {
                "uid": current_user.id,
                "tid": current_user.team_id,
                "model": model,
                "pt": response_data["usage"]["prompt_tokens"],
                "ct": response_data["usage"]["completion_tokens"],
                "tt": response_data["usage"]["total_tokens"],
                "dur": duration_ms
            }
        )

    return response_data

Aggregated Team Usage Query

# Python: Team usage aggregation query
async def get_team_usage_breakdown(
    team_id: str,
    start_date: str,
    end_date: str,
    db: AsyncSession
) -> list[dict]:
    result = await db.execute(
        """
        SELECT
            u.name AS user_name,
            u.email AS user_email,
            aul.model,
            SUM(aul.prompt_tokens) AS total_prompt_tokens,
            SUM(aul.completion_tokens) AS total_completion_tokens,
            SUM(aul.total_tokens) AS total_tokens,
            COUNT(*) AS request_count,
            AVG(aul.duration_ms) AS avg_duration_ms,
            -- Cost calculation: GPT-4o rates
            SUM(
                CASE WHEN aul.model = 'gpt-4o'
                THEN (aul.prompt_tokens * 0.000005 + aul.completion_tokens * 0.000015)
                WHEN aul.model = 'gpt-3.5-turbo'
                THEN (aul.prompt_tokens * 0.0000005 + aul.completion_tokens * 0.0000015)
                ELSE 0 END
            ) AS estimated_cost_usd
        FROM api_usage_log aul
        JOIN users u ON u.id = aul.user_id
        WHERE aul.team_id = :team_id
          AND aul.created_at BETWEEN :start_date AND :end_date
        GROUP BY u.id, u.name, u.email, aul.model
        ORDER BY total_tokens DESC
        """,
        {"team_id": team_id, "start_date": start_date, "end_date": end_date}
    )
    return [dict(row) for row in result.fetchall()]

Identifying Heavy Users and Optimizing Allocation

Raw consumption numbers alone don't tell the full story. A developer writing automated test suites that call GPT-4o 1,000 times per day may be generating far more value than a developer who makes 10 manual calls. The team monitoring dashboard should present consumption alongside output metrics where available: lines of code generated per token, documentation pages created per request, or task completion rates for Codex workloads.

User Monthly Tokens Est. Cost Requests Avg Tokens/Request Primary Model
Sarah K. 4.2M $84.00 2,840 1,479 GPT-4o
Marcus T. 3.8M $19.00 12,400 306 GPT-3.5 Turbo
Priya N. 1.1M $22.00 920 1,196 GPT-4 Turbo
James W. 890K $4.45 8,200 109 GPT-3.5 Turbo

Notice that Marcus drives high request volume at low cost by correctly using GPT-3.5 Turbo for high-frequency, low-complexity tasks. This is exactly the usage pattern you want to reward and replicate across the team. Budget conversations become data-driven rather than anecdotal when you can present this breakdown to engineering managers.

How to Track Your ChatGPT and Codex Usage with the New Usage Data API: Complete Developer Integration Guide - Section 2

Step 7: Integrating with Billing Systems

Connecting OpenAI usage data to your billing infrastructure transforms usage monitoring from a developer tool into a finance tool. This integration is essential for teams operating under chargeback models, where individual business units or departments are responsible for their own AI infrastructure costs.

Cost Calculation Layer

OpenAI's published pricing is the foundation of your cost calculations. Maintaining a pricing configuration file rather than hardcoding rates makes it straightforward to update when pricing changes:

# Python: Pricing configuration and cost calculator
OPENAI_PRICING = {
    "gpt-4o": {
        "prompt_per_1k": 0.005,
        "completion_per_1k": 0.015,
        "cached_prompt_per_1k": 0.0025
    },
    "gpt-4o-mini": {
        "prompt_per_1k": 0.00015,
        "completion_per_1k": 0.0006,
        "cached_prompt_per_1k": 0.000075
    },
    "gpt-4-turbo": {
        "prompt_per_1k": 0.01,
        "completion_per_1k": 0.03,
        "cached_prompt_per_1k": 0.005
    },
    "gpt-3.5-turbo": {
        "prompt_per_1k": 0.0005,
        "completion_per_1k": 0.0015,
        "cached_prompt_per_1k": 0.00025
    }
}

def calculate_cost(
    model: str,
    prompt_tokens: int,
    completion_tokens: int,
    cached_prompt_tokens: int = 0
) -> float:
    """Returns cost in USD"""
    pricing = OPENAI_PRICING.get(model, OPENAI_PRICING["gpt-3.5-turbo"])

    # Cached tokens are billed separately at reduced rate
    non_cached_prompt = prompt_tokens - cached_prompt_tokens

    cost = (
        (non_cached_prompt / 1000) * pricing["prompt_per_1k"] +
        (cached_prompt_tokens / 1000) * pricing["cached_prompt_per_1k"] +
        (completion_tokens / 1000) * pricing["completion_per_1k"]
    )
    return round(cost, 6)

Department Chargeback Report Generator

# Python: Monthly chargeback report generation
from datetime import datetime
import pandas as pd

async def generate_chargeback_report(
    billing_month: str,  # Format: "2024-11"
    db: AsyncSession
) -> dict:
    """
    Generate department-level chargeback data for finance integration.
    Returns structured data suitable for export to Stripe, QuickBooks, or CSV.
    """
    rows = await db.execute(
        """
        SELECT
            d.name AS department,
            d.cost_center_code,
            d.billing_contact_email,
            SUM(aul.total_tokens) AS total_tokens,
            SUM(calculated_cost) AS total_cost_usd,
            COUNT(DISTINCT aul.user_id) AS active_users,
            COUNT(*) AS total_requests
        FROM api_usage_log aul
        JOIN users u ON u.id = aul.user_id
        JOIN departments d ON d.id = u.department_id
        WHERE DATE_FORMAT(aul.created_at, '%%Y-%%m') = :billing_month
        GROUP BY d.id, d.name, d.cost_center_code, d.billing_contact_email
        ORDER BY total_cost_usd DESC
        """,
        {"billing_month": billing_month}
    )

    departments = [dict(row) for row in rows.fetchall()]
    total_cost = sum(d["total_cost_usd"] for d in departments)

    return {
        "billing_month": billing_month,
        "generated_at": datetime.utcnow().isoformat(),
        "total_organization_cost_usd": round(total_cost, 2),
        "departments": [
            {
                **dept,
                "total_cost_usd": round(dept["total_cost_usd"], 2),
                "cost_percentage": round(
                    (dept["total_cost_usd"] / total_cost) * 100, 2
                ) if total_cost > 0 else 0,
                "cost_per_user_usd": round(
                    dept["total_cost_usd"] / dept["active_users"], 2
                ) if dept["active_users"] > 0 else 0
            }
            for dept in departments
        ]
    }

ROI Calculation Framework

Pure cost tracking without ROI context produces cost anxiety rather than optimization insight. A simple ROI framework pairs AI consumption costs against measurable outcomes. For engineering teams, the most accessible metric is developer time saved:

# Python: ROI calculation for code generation use cases
def calculate_code_gen_roi(
    monthly_cost_usd: float,
    lines_of_code_generated: int,
    avg_hourly_developer_rate: float = 85.0,
    lines_per_developer_hour: int = 40  # Conservative estimate
) -> dict:
    """
    Estimates ROI for AI-assisted code generation investment.
    """
    developer_hours_equivalent = lines_of_code_generated / lines_per_developer_hour
    developer_cost_equivalent = developer_hours_equivalent * avg_hourly_developer_rate

    roi_multiple = developer_cost_equivalent / monthly_cost_usd if monthly_cost_usd > 0 else 0
    net_savings = developer_cost_equivalent - monthly_cost_usd

    return {
        "monthly_ai_cost_usd": round(monthly_cost_usd, 2),
        "lines_generated": lines_of_code_generated,
        "developer_hours_equivalent": round(developer_hours_equivalent, 1),
        "developer_cost_equivalent_usd": round(developer_cost_equivalent, 2),
        "net_savings_usd": round(net_savings, 2),
        "roi_multiple": round(roi_multiple, 2),
        "roi_percentage": round((roi_multiple - 1) * 100, 1)
    }

Understanding how AI API costs fit into broader product infrastructure spend is covered in depth in OpenAI API Cost Optimization Strategies for Production Applications.

Step 8: Error Handling and Rate Limits

Production usage monitoring systems must handle API errors gracefully. The Usage Data API can return several error classes, and your application's behavior in each case should be carefully designed.

Error Response Schema

# Possible error responses from the Usage Data API
{
  "error": {
    "type": "authentication_error",
    "code": "invalid_api_key",
    "message": "No API key provided or key is invalid.",
    "status": 401
  }
}

{
  "error": {
    "type": "rate_limit_error",
    "code": "usage_api_rate_limit_exceeded",
    "message": "You have exceeded the usage API rate limit of 120 requests/minute.",
    "status": 429,
    "retry_after": 12
  }
}

{
  "error": {
    "type": "server_error",
    "code": "usage_data_unavailable",
    "message": "Usage data is temporarily unavailable. Retry after 60 seconds.",
    "status": 503,
    "retry_after": 60
  }
}

Robust Retry Logic with Exponential Backoff

# Python: Production-grade retry handler
import asyncio
from typing import TypeVar, Callable, Any
import httpx

T = TypeVar('T')

async def with_retry(
    func: Callable[[], Any],
    max_attempts: int = 3,
    base_delay: float = 1.0,
    max_delay: float = 60.0
):
    last_exception = None

    for attempt in range(max_attempts):
        try:
            return await func()
        except httpx.HTTPStatusError as e:
            last_exception = e
            status = e.response.status_code

            if status == 401:
                # Authentication errors are not retryable
                raise RuntimeError(
                    "Usage API authentication failed. Check OPENAI_USAGE_API_KEY."
                ) from e

            if status == 429:
                # Respect the Retry-After header if present
                retry_after = float(e.response.headers.get("Retry-After", base_delay))
                await asyncio.sleep(min(retry_after, max_delay))
                continue

            if status >= 500:
                # Server errors: exponential backoff
                delay = min(base_delay * (2 ** attempt), max_delay)
                await asyncio.sleep(delay)
                continue

            # 4xx errors other than 401/429 are not retryable
            raise

        except httpx.NetworkError:
            delay = min(base_delay * (2 ** attempt), max_delay)
            await asyncio.sleep(delay)

    raise last_exception

Circuit Breaker Pattern

For usage monitoring systems that run as background services, a circuit breaker prevents your monitoring infrastructure from hammering a degraded Usage API and accumulating unnecessary errors in your logs:

# Python: Simple circuit breaker for usage API calls
from enum import Enum
from datetime import datetime, timedelta

class CircuitState(Enum):
    CLOSED = "closed"      # Normal operation
    OPEN = "open"          # Failing fast, not making calls
    HALF_OPEN = "half_open"  # Testing recovery

class UsageAPICircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=300):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = CircuitState.CLOSED

    def record_success(self):
        self.failure_count = 0
        self.state = CircuitState.CLOSED

    def record_failure(self):
        self.failure_count += 1
        self.last_failure_time = datetime.utcnow()
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN

    def can_execute(self) -> bool:
        if self.state == CircuitState.CLOSED:
            return True
        if self.state == CircuitState.OPEN:
            recovery_due = (
                self.last_failure_time +
                timedelta(seconds=self.recovery_timeout)
            )
            if datetime.utcnow() >= recovery_due:
                self.state = CircuitState.HALF_OPEN
                return True
            return False
        return True  # HALF_OPEN: allow one test request

usage_circuit_breaker = UsageAPICircuitBreaker()

Step 9: Caching Best Practices

Caching is not an optimization for usage monitoring — it's a fundamental architectural requirement. Usage data doesn't change in real time at a resolution that justifies frequent polling, and your dashboard consumers (browsers, reports, alert checks) should never directly hit the Usage API. The caching layer you build here will also protect you against Usage API outages causing your monitoring dashboard to go dark.

Cache TTL Strategy by Endpoint

Endpoint Update Frequency Recommended Cache TTL Rationale
/v1/usage/summary Near real-time 5 minutes Balances freshness with API load
/v1/usage/history Daily 1 hour Historical data changes slowly
/v1/usage/codex Per-task completion 5 minutes Active Codex runs need visibility
Team breakdown queries Your own DB 15 minutes Aggregates are expensive to compute
Chargeback reports Monthly 24 hours Immutable once billing cycle closes

Redis Cache Implementation

# Python: Redis-based caching layer for usage data
import json
import redis.asyncio as aioredis
from functools import wraps

redis_client = aioredis.from_url(
    os.environ["REDIS_URL"],
    encoding="utf-8",
    decode_responses=True
)

def cache_usage_response(ttl_seconds: int, key_prefix: str):
    """Decorator for caching Usage API responses in Redis"""
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            # Build cache key from function name and arguments
            cache_key = f"usage:{key_prefix}:{hash(str(args) + str(kwargs))}"

            # Check cache first
            cached = await redis_client.get(cache_key)
            if cached:
                return json.loads(cached)

            # Cache miss: call the actual function
            result = await func(*args, **kwargs)

            # Store in cache with TTL
            await redis_client.setex(
                cache_key,
                ttl_seconds,
                json.dumps(result)
            )

            return result
        return wrapper
    return decorator

@cache_usage_response(ttl_seconds=300, key_prefix="summary")
async def get_cached_usage_summary() -> dict:
    return await fetch_usage_summary()

@cache_usage_response(ttl_seconds=3600, key_prefix="history")
async def get_cached_usage_history(start_date: str, end_date: str) -> dict:
    return await fetch_usage_history(start_date, end_date)

Cache Invalidation on Alert Triggers

When an alert fires at the 80% or 95% threshold, you want the dashboard to reflect the most current data immediately. Implement a forced cache invalidation that triggers on every alert event:

# Python: Forced cache invalidation for alert triggers
async def invalidate_usage_cache():
    """
    Force-invalidate all cached usage data when alerts trigger.
    Call this before sending alert notifications to ensure
    dashboard users see current data.
    """
    pattern = "usage:summary:*"
    keys = await redis_client.keys(pattern)
    if keys:
        await redis_client.delete(*keys)

    # Also clear the codex cache if Codex limits are involved
    codex_pattern = "usage:codex:*"
    codex_keys = await redis_client.keys(codex_pattern)
    if codex_keys:
        await redis_client.delete(*codex_keys)

async def trigger_alert_with_cache_refresh(alert: UsageAlert):
    await invalidate_usage_cache()
    await send_slack_alert(alert)
    if alert.level == AlertLevel.CRITICAL:
        await send_email_alert(alert)

Stale-While-Revalidate Pattern

For dashboard components where stale data is acceptable (historical charts, team breakdowns), implement a stale-while-revalidate pattern that returns cached data immediately while refreshing in the background. This keeps your dashboard responsive even during slow API conditions:

# Python: Stale-while-revalidate implementation
import asyncio
from datetime import datetime

class StaleWhileRevalidateCache:
    def __init__(self, redis_client, ttl: int, stale_ttl: int):
        self.redis = redis_client
        self.ttl = ttl
        self.stale_ttl = stale_ttl  # How long to serve stale data

    async def get_or_fetch(self, key: str, fetch_fn) -> tuple[dict, bool]:
        """
        Returns (data, is_stale) tuple.
        Triggers background refresh if data is stale.
        """
        cached_raw = await self.redis.get(f"data:{key}")
        meta_raw = await self.redis.get(f"meta:{key}")

        if cached_raw and meta_raw:
            meta = json.loads(meta_raw)
            cached_at = datetime.fromisoformat(meta["cached_at"])
            age_seconds = (datetime.utcnow() - cached_at).total_seconds()

            if age_seconds < self.ttl:
                return json.loads(cached_raw), False  # Fresh data

            if age_seconds < self.stale_ttl:
                # Serve stale data, trigger background refresh
                asyncio.create_task(self._background_refresh(key, fetch_fn))
                return json.loads(cached_raw), True  # Stale data

        # Cache miss or too stale: fetch synchronously
        fresh_data = await fetch_fn()
        await self._store(key, fresh_data)
        return fresh_data, False

    async def _background_refresh(self, key: str, fetch_fn):
        try:
            fresh_data = await fetch_fn()
            await self._store(key, fresh_data)
        except Exception:
            pass  # Background refresh failures are non-fatal

    async def _store(self, key: str, data: dict):
        pipeline = self.redis.pipeline()
        pipeline.setex(f"data:{key}", self.stale_ttl, json.dumps(data))
        pipeline.setex(f"meta:{key}", self.stale_ttl, json.dumps({
            "cached_at": datetime.utcnow().isoformat()
        }))
        await pipeline.execute()

For teams working with complex API integrations, understanding the full scope of what's possible with OpenAI's platform APIs is valuable. Complete Guide to OpenAI Platform API Features and Enterprise Capabilities provides comprehensive coverage of the broader API ecosystem.

Conclusion and Next Steps

Building a production-grade usage monitoring system for your OpenAI consumption is a multi-layer engineering effort that pays dividends across engineering, finance, and operations teams. Let's summarize what you've built through this guide:

What You've Built

  • API Integration Layer: Full Python and JavaScript clients for all three Usage Data API endpoints, with proper authentication handling for both API key and OAuth 2.0 flows
  • React Dashboard: Real-time usage visualization with progress bars, historical trend charts, per-model breakdowns, and a burn rate predictor that flags when current spending patterns will exceed the billing cycle allowance
  • Alert System: Threshold evaluation at 80% and 95% utilization with Slack and email notifications, webhook receiver with HMAC signature verification, and cache invalidation on alert triggers
  • Team Attribution: Request tagging proxy middleware that attributes API consumption to individual users and departments without requiring changes to the OpenAI API itself
  • Billing Integration: Cost calculation engine using real pricing data, department chargeback report generation, and an ROI framework for justifying AI infrastructure investment
  • Production Infrastructure: Retry logic with exponential backoff, circuit breaker pattern, Redis caching with appropriate TTLs by data type, and stale-while-revalidate for dashboard responsiveness

Recommended Implementation Sequence

  1. Start with the basic summary endpoint integration and a simple utilization display — this delivers immediate value in hours
  2. Add Redis caching before scaling the polling frequency, not after
  3. Deploy the alert system next; catching a 95% utilization event before it becomes an overage is worth days of development time
  4. Build the proxy middleware for team attribution in parallel with the dashboard — it needs time to accumulate data before the breakdown views are meaningful
  5. Add billing integration last, once you have 30+ days of attribution data to make the chargeback reports credible

Future Enhancements

As your usage monitoring system matures, consider adding anomaly detection (flagging usage spikes that deviate significantly from historical patterns), cost forecasting models trained on your own consumption history, and per-project attribution by adding project identifiers to your request tagging proxy. For teams using OpenAI's fine-tuned models, extending the cost calculation layer to include fine-tuning run costs rounds out the complete picture of your AI infrastructure spend.

The Usage Data API is your foundation for treating AI infrastructure costs with the same rigor you apply to cloud compute, storage, and bandwidth. Teams that build this monitoring infrastructure early develop the institutional knowledge to scale their AI usage responsibly — and to confidently justify continued investment because they can prove, with data, that it's working.

For deeper exploration of managing and optimizing production AI deployments, Production OpenAI API Architecture Patterns for Scalable Applications covers the broader system design considerations that complement the monitoring infrastructure you've built here.

Get Free Access to 40,000+ AI Prompts for ChatGPT, Claude & Codex

Subscribe for instant access to the largest curated Notion Prompt Library for AI workflows.

More on this