土曜の昼、旧 Usage and Cost API の移行表を上から順に読んでいて、ある一行で手が止まりました。旧 API の data は「空のバケットも含む時間バケットの一覧」、新しい Analytics API は「使用量のある組み合わせだけの行の一覧」とあります。
日別の使用量を表にして眺める習慣のある方なら、ここで嫌な予感がするかもしれません。使わなかった日が、表からそのまま消えるのです。
最初にお伝えしておくと、私の手元は個人のアカウントで、Admin API 自体が使えません(公式ページに「個人アカウントでは利用できない」と明記されています)。ですので以下は、本番の応答ではなく、公式のサンプルと同じ形に作ったモックで確かめた内容です。その前提で読んでいただければ幸いです。
日が欠けて見える原因は 3 つあります
移行表から、日別の表づくりに効く差だけを拾うと次のとおりです。
| 項目 | 旧 Usage and Cost API | Usage and Cost Analytics API |
|---|---|---|
| 空の日・空の組み合わせ | 空のバケットも返る | 行が作られない |
| 日別にする指定 | bucket_width(既定 1d) | time_bucket_width(既定なし) |
limit の数え方 | 時間バケットの数 | 行の数(1〜1,000、既定 20) |
| データの反映 | おおむね 5 分以内 | おおむね 4 時間以内、最大 24 時間 |
| 1 回で引ける範囲 | 制限なし | 最大 31 日、1 分刻みは 24 時間 |
つまり、旧 API の感覚で呼ぶと、少なくとも次の 3 通りで「日が足りない」表ができあがります。
- 使用量のなかった日は、そもそも行が来ません。
time_bucket_widthを付けないと、期間全体を合計した 1 行しか返りません。limitを付けないと 1 ページ 20 行で止まり、そのあとのページを読み忘れがちです。
症状から原因を切り分ける順番
私は次の順で見るのが早いと考えています。画面や数字の見え方ごとに、先に疑う場所が違うからです。
| 見えているもの | 先に疑うこと | 確かめ方 |
|---|---|---|
| 行が 1 つだけ返る | time_bucket_width の付け忘れ | リクエストに time_bucket_width=1d があるか |
| 7 日のはずが 4〜5 日分しかない | 使用量のない日の欠落 | 欠けた日のログを見て、本当に呼んでいないか |
| モデルを増やすと行数が急に増える | 1 組み合わせにつき 1 行 | group_by[] の数と行数の関係 |
| 途中の日から先がない | ページ送り、または反映待ち | has_more と data_refreshed_at |
とくに最後の行は、欠落と反映待ちが同じ見た目になります。応答に必ず付いてくる data_refreshed_at より後の使用量は、欠けているか、途中までしか入っていません。直近の日が少なく見えるのは、バグではなく仕様です。
日別の表を、ゼロ埋めで作り直す
公式の移行例にも「欠けた日はゼロで埋める」と書かれています。私は、受け取った行をモデル別に畳んで、期間内の全日を並べ直す小さな関数にしました。
from datetime import datetime, timedelta, timezone
from collections import defaultdict
def fill_daily(rows, start, end, key="model"):
"""Analytics API の usage 行(time_bucket_width=1d)を、欠けた日をゼロで埋めて返す。"""
days = []
d = start
while d < end:
days.append(d)
d += timedelta(days=1)
table = defaultdict(lambda: {day: 0 for day in days})
for row in rows:
day = datetime.fromisoformat(row["starting_at"].replace("Z", "+00:00"))
name = row["group"][key] or "(none)"
table[name][day] += row["usage"]["total_tokens"]
return {k: [(day, v[day]) for day in days] for k, v in table.items()}呼び出し側は、公式の移行例と同じ形にします。time_bucket_width を明示し、limit は 1,000 に上げ、SDK が残りのページを読んでくれる前提で for 文で回します。
import anthropic
client = anthropic.Anthropic() # 環境変数から Admin 権限のキーを読む
rows = client.beta.organization.analytics.usage.list(
starting_at="2026-09-01T00:00:00Z",
ending_at="2026-09-08T00:00:00Z",
time_bucket_width="1d",
group_by=["model"],
limit=1000,
)
as_dicts = [r.model_dump() for r in rows]本番のキーでは試せませんので、fill_daily はモックで動かしました。9 月 1 日と 3 日にだけ使用量がある行を渡し、1 日〜7 日の 7 日分を並べた結果です。
claude-sonnet-5 [5590800, 0, 120000, 0, 0, 0, 0]
(none) [0, 0, 0, 0, 0, 0, 0]2 日の欠けが 0 で埋まり、model が空の行は (none) に寄りました。model が null になる行は、公式の説明どおりコード実行などの使用量です。ここで null のまま辞書のキーに使うと、後工程で並べ替えが落ちやすいので、私は先に名前を付けています。
それでも合わないときに見る 3 つの場所
ゼロ埋めをしても数字が合わないときは、次の 3 つを順に確かめます。
ending_atをdata_refreshed_at以前に置いてください。 同じ範囲を繰り返し引いたとき、数字が日によって変わる原因の多くは、反映途中の日を含めていることです。- 確定値は、月が終わって 7 日以上たってから見てください。 公式には、月次の締め後およそ 7 日間は値が改定されうると書かれています。10 月 1 日の値が 11 月 7 日ごろまで動く、という例が添えられています。
- 範囲は 31 日以内に切ってください。 長い期間は 1 回では引けません。月跨ぎの表は、2 回に分けて貼り合わせる前提で組みます。
旧 API と画面の数字が合わなかった方は、まずこの 3 つを疑うのが近道です。旧 API は 5 分ほどで反映されていましたので、「昨日は合っていたのに」という感覚が、そのまま新 API の反映の遅さに突き当たるのだと思います。
旧 API は、止めずに並べて見比べます
ここまで書いて、あらためて確かめたことがあります。旧 API は「レガシーだが引き続きサポートされる」と公式に書かれており、新規の実装に新 API を勧める、という立て付けです。急いで差し替える必要はありません。
新しい表は、古い表と並べて合わせてから切り替えます。 私はこの線引きにしました。旧 API の日別レポートに、新 API で作った同じ期間のゼロ埋めの表を横に置き、直近の 1 週間は差が出ても気にせず、締めの済んだ前月だけを突き合わせます。差が消えたことを確かめてから、集計の元を切り替えます。
月次の費用をワークスペース別に配る運用をされている方は、拙稿「ワークスペース単位で支出を割り当て、絞るパイプラインを見極める」の考え方がそのまま使えます。入口のエンドポイントが替わるだけで、配り方は変わりません。
まずは、いま使っている集計スクリプトの bucket_width を一度検索して、新 API で必要になる time_bucket_width と limit の置き場所を、メモに 1 行ずつ書き出すところから始めていただければと思います。