How can I track emissions from AI API calls for carbon accounting?
For engineers who own the code that calls AI APIs and need numbers for the carbon team. Factor version soma-ai-ef-2026.11-dataset-v3.1.0. Factors last updated . Location-based grid averages, not renewable contracts. Estimates from published benchmarks.
Page updated
Part of the SOMA AI Emission Factors cluster.
Short answer
Log the input and output token counts that every API response returns, multiply the total by the factor for the model's size class and serving region from the open SAEF CSV, and sum by month. SOMA AI publishes the CSV under CC BY 4.0 and its columns are named below. A million tokens of a mid-size model from US East - N. Virginia is 0.0261 kg CO2e as an estimate.
Or use the Python package
The saef package on PyPI applies the full v3 method to your logged usage. Install it with pip install saef. Pass input and output tokens, a model and a region, and it returns a central estimate, a conservative high case, water and the provenance of every number.
import saef
usage = {"input_tokens": 1_500_000, "output_tokens": 400_000}
r = saef.estimate(**usage, model="gpt-4o-mini", region="eu-west-1")
print(f"class {r.model_class}, billed {r.billed_tokens:,.0f} tokens, effective {r.effective_tokens:,.0f}")
print(f"central {r.kg_co2e:.4f} kg CO2e, high case {r.kg_co2e_high_case:.4f} kg CO2e")
print(f"water {r.water_ml:.1f} mL")Printed output.
class A, billed 1,900,000 tokens, effective 634,802 central 0.0149 kg CO2e, high case 0.0236 kg CO2e water 67.9 mL
The package weights tokens by your actual input and output mix and converts them to effective tokens. The CSV snippets below apply one per-million-token factor for the default mix, so the same 1,900,000 billed tokens give 0.0136 kg CO2e there. The gap is the method, billed tokens against effective tokens, not a different dataset. Both are estimates. The package is MIT licensed and the data is CC BY 4.0. Source on GitHub.
Steps
- Read the usage field of every response. Each provider returns billed token counts with the reply. Store input and output tokens, the model id, a timestamp and the region you believe served it.
- Load the factor table once. Download the SAEF CSV, keep a dated copy with the file's factor_version, and key rows by region_slug and model_class.
- Multiply at logging time. Divide total tokens by one million and multiply by kg_co2e_per_million_tokens_total for the row. Write the result and the factor_version into the log line.
- Aggregate by month and model. Sum the logged kilograms by calendar month, model and region. Keep the raw token counts so the figures can be recomputed when a factor is updated.
Where the token counts come from
| Provider | Input | Output |
|---|---|---|
| OpenAI, Chat Completions | usage.prompt_tokens | usage.completion_tokens |
| OpenAI, Responses | usage.input_tokens | usage.output_tokens |
| Anthropic | usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens | usage.output_tokens |
| Gemini | usage_metadata.prompt_token_count | usage_metadata.candidates_token_count + usage_metadata.thoughts_token_count |
Python
The CSV columns used are region_slug, model_class, kg_co2e_per_million_tokens_total and factor_version. The model class is A, B or C and you choose it, since the CSV does not map model names.
import csv, io, json, urllib.request
from collections import defaultdict
from datetime import datetime, timezone
CSV_URL = "https://somaai.earth/ai-footprint/saef-factors.csv"
with urllib.request.urlopen(CSV_URL) as r:
rows = csv.DictReader(io.StringIO(r.read().decode("utf-8")))
FACTORS = {(x["region_slug"], x["model_class"]): x for x in rows}
def log_call(model, model_class, region_slug, input_tokens, output_tokens, path="ai_usage.jsonl"):
f = FACTORS[(region_slug, model_class)]
tokens = input_tokens + output_tokens
kg = tokens / 1e6 * float(f["kg_co2e_per_million_tokens_total"])
record = {
"ts": datetime.now(timezone.utc).isoformat(),
"model": model, "model_class": model_class, "region_slug": region_slug,
"input_tokens": input_tokens, "output_tokens": output_tokens,
"kg_co2e": kg, "factor_version": f["factor_version"],
}
with open(path, "a") as out:
out.write(json.dumps(record) + "\n")
return kg
def monthly(path="ai_usage.jsonl"):
totals = defaultdict(float)
with open(path) as src:
for line in src:
r = json.loads(line)
totals[(r["ts"][:7], r["model"], r["region_slug"])] += r["kg_co2e"]
return dict(totals)
# after a call, for example with an OpenAI-style response
# log_call("my-model", "B", "us-east", resp.usage.prompt_tokens, resp.usage.completion_tokens)JavaScript
const CSV_URL = "https://somaai.earth/ai-footprint/saef-factors.csv";
async function loadFactors() {
const text = await (await fetch(CSV_URL)).text();
const [header, ...lines] = text.trim().split(/\r?\n/);
const cols = header.split(",");
const factors = {};
for (const line of lines) {
const values = line.split(",");
const row = Object.fromEntries(cols.map((c, i) => [c, values[i]]));
factors[row.region_slug + "/" + row.model_class] = row;
}
return factors;
}
function logCall(factors, log, { model, modelClass, regionSlug, inputTokens, outputTokens }) {
const f = factors[regionSlug + "/" + modelClass];
const kg = ((inputTokens + outputTokens) / 1e6) * Number(f.kg_co2e_per_million_tokens_total);
log.push({ ts: new Date().toISOString(), model, regionSlug, inputTokens, outputTokens, kg, factorVersion: f.factor_version });
return kg;
}
function monthly(log) {
const totals = {};
for (const r of log) {
const key = r.ts.slice(0, 7) + " " + r.model + " " + r.regionSlug;
totals[key] = (totals[key] ?? 0) + r.kg;
}
return totals;
}
// after a call, for example with an Anthropic response
// logCall(factors, log, { model: "my-model", modelClass: "B", regionSlug: "us-east",
// inputTokens: msg.usage.input_tokens + (msg.usage.cache_read_input_tokens ?? 0) + (msg.usage.cache_creation_input_tokens ?? 0),
// outputTokens: msg.usage.output_tokens });Both snippets use the default mix of input and output tokens and take no credit for cached input. That keeps the arithmetic to one line and slightly overstates a workload with a lot of cache hits. For an exact split, use the full formula on the formula page or the partner factors API, which needs a key from SOMA AI.
Worked example
One month of logged calls, all served from US East - N. Virginia (us-east in the CSV).
| Model | Calls | Tokens | Kg CO2e per M | Kg CO2e |
|---|---|---|---|---|
| Mid-size model (class B) | 12,000 | 22,800,000 | 0.0261 | 0.594 |
| Frontier model (class C) | 2,000 | 13,800,000 | 0.0991 | 1.37 |
| Month total | 1.96 |
Where to get the data
The CSV is at https://somaai.earth/ai-footprint/saef-factors.csv. For an audit trail, pin a copy by DOI 10.5281/zenodo.20443585, or take it from GitHub or Hugging Face. The SOMA AI calculator does the same without code.
Common mistakes
- Logging only the total. Keep input and output apart, so a better factor can be applied later.
- Hard-coding a factor with no version. Store the factor_version beside every result.
- Guessing the region from your own office. Use the region the vendor serves from, or label it an assumption.
Keep reading
- AI carbon calculator that uses real model and region data. What a calculator needs and the open options
- How do I calculate the carbon footprint of my company's AI usage?
- What is the formula to estimate CO2e from LLM API token usage? The arithmetic in full
- What should I ask an AI vendor for to report the carbon footprint of my usage? When the region is not in the response
