История списаний через GET /v1/usage для своей отчётности

Если нужно посчитать, сколько потратил каждый клиент или проект, дергать кабинет руками не вариант. Разбираем, как выгрузить всю историю через API.

5 мин · · Обновлено · API · разработка

Когда через один ключ идёт трафик от нескольких проектов или клиентов, рано или поздно встаёт вопрос: кто сколько потратил. Кабинет показывает баланс и последние операции, но для отчёта в конце месяца этого мало — нужна полная выгрузка с разбивкой по моделям и датам. За это отвечает GET /v1/usage. Сразу оговорка: история в нём общая на весь аккаунт (все ключи вместе), фильтра по ключу нет, поэтому разбивку по клиентам или проектам придётся вести у себя — об этом ниже.

Что возвращает эндпоинт

GET /v1/usage — это история списаний с курсорной пагинацией. Нужен ключ в заголовке Authorization: Bearer fetch_.... Каждая запись в истории соответствует одному оплаченному запросу к модели и содержит поля created_at, model, input_tokens, output_tokens, total_tokens и cost_cents (стоимость в дробных центах); курсор следующей страницы приходит в next_cursor. Попадания в кэш ответов и пополнения баланса в историю не пишутся. Списания за неудачные запросы нет, потому что баланс проверяется до похода к провайдеру, и оплаченных записей от ошибок 401/402/404 там просто не будет.

Базовый запрос:

bash
curl https://api.zukko.pro/v1/usage \
  -H "Authorization: Bearer fetch_ваш_ключ"

Курсорная пагинация означает, что вместо номеров страниц сервер отдаёт указатель на следующий кусок данных. Логика стандартная для такого подхода: получаете первую порцию, берёте из ответа курсор для продолжения, подставляете его в следующий запрос, повторяете, пока курсор не закончится.

bash
curl "https://api.zukko.pro/v1/usage?cursor=NEXT_CURSOR" \
  -H "Authorization: Bearer fetch_ваш_ключ"

Скрипт для полной выгрузки

Для отчёта обычно нужна не одна страница, а весь период целиком. Питоновский скрипт, который проходит по всем страницам и складывает записи в список:

bash
import requests

API_KEY = "fetch_ваш_ключ"
BASE = "https://api.zukko.pro/v1/usage"

records = []
cursor = None

while True:
    params = {"cursor": cursor} if cursor else {}
    resp = requests.get(BASE, headers={"Authorization": f"Bearer {API_KEY}"}, params=params)
    resp.raise_for_status()
    data = resp.json()

    records.extend(data.get("data", []))
    cursor = data.get("next_cursor")

    if not cursor:
        break

print(f"Всего записей: {len(records)}")

Если ключей несколько (например, по одному на клиента), гонять скрипт по каждому ключу отдельно бессмысленно: история общая на аккаунт, и любой ключ вернёт одни и те же записи всех ключей. Чтобы разделить расход по клиентам или проектам, логируйте у себя X-Zukko-Cost-Micros из ответов по каждому ключу (для нестриминговых запросов) или заведите отдельный аккаунт на клиента. Баланс тоже один — у аккаунта, а не у ключа.

Если стек на Node, а не Python

Та же логика пагинации на JavaScript, без сторонних библиотек:

bash
const API_KEY = "fetch_ваш_ключ";
const BASE = "https://api.zukko.pro/v1/usage";

async function fetchAllUsage() {
  const records = [];
  let cursor = null;

  while (true) {
    const url = cursor ? `${BASE}?cursor=${cursor}` : BASE;
    const resp = await fetch(url, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });
    const data = await resp.json();

    records.push(...(data.data || []));
    cursor = data.next_cursor;

    if (!cursor) break;
  }

  return records;
}

Логика идентична питоновскому варианту: тянуть страницы, пока сервер отдаёт next_cursor, и останавливаться, как только он приходит пустым. Если отчёт собирается в CI или в отдельном воркере на Node, эту функцию можно вызывать раз в день по расписанию и складывать результат в свою базу, не трогая питоновский стек вообще.

Агрегация по моделям и дням

Сырой список записей неудобен для отчёта, поэтому его сразу же сворачивают в таблицу. Если в каждой записи есть модель и дата запроса, агрегация — это обычный defaultdict:

bash
from collections import defaultdict
from datetime import datetime

by_day_model = defaultdict(float)

for r in records:
    day = r["created_at"][:10]
    model = r["model"]
    cost_usd = r["cost_cents"] / 100  # cost_cents — дробные центы
    by_day_model[(day, model)] += cost_usd

for (day, model), cost in sorted(by_day_model.items()):
    print(f"{day}  {model:30s}  ${cost:.4f}")

Дальше это либо уходит в CSV для бухгалтерии, либо ложится в собственную биллинговую систему, если вы перепродаёте доступ клиентам поверх своего ключа (разбивку по клиентам берите из своих логов — в /v1/usage её нет).

Сверка с заголовками ответа

У каждого обычного (не потокового) запроса к модели в ответе есть заголовок X-Zukko-Cost-Micros — стоимость именно этого запроса в микродолларах (1 000 000 = $1), и X-Fetch-Balance-Cents — остаток баланса после списания (несмотря на название, тоже в микродолларах; точный баланс в центах — поле balance_cents в GET /v1/balance). У ответов с stream: true этих заголовков нет — для потоковых клиентов (Cline, Roo, Claude Code, Zed, aider) остаётся /v1/usage, /v1/balance и кабинет. Если в проде уже логируются ответы API, эти два заголовка можно писать в свои логи в реальном времени и не ждать выгрузки через /v1/usage вообще — это быстрее для мониторинга «сколько потратили за последний час», а /v1/usage использовать для сверки и отчётов задним числом.

Отдельно полезно сверять сумму по /v1/usage с текущим балансом через GET /v1/balance (с поправкой на пополнения — их в /v1/usage нет) — если числа не бьются, скорее всего где-то в логировании потерялись ответы с ошибкой или дубли из-за ретраев.

Что может пойти не так

Частая выгрузка не нужна. Rate limit у /v1/usage нет, и общего rate limit у шлюза тоже нет; 429 с key_daily_limit_reached / key_monthly_limit_reached относится к лимитам расхода ключа на запросы к моделям, а не к чтению истории. Но и дёргать /v1/usage раз в секунду из cron незачем: для регулярной отчётности достаточно тянуть данные раз в час или раз в день, а для живого мониторинга удобнее заголовки ответа.

Пустой next_cursor — это конец, а не ошибка. Цикл пагинации должен останавливаться, когда курсор в ответе отсутствует или пуст, а не пытаться подставить пустую строку в следующий запрос — это одна из типичных причин зависшего скрипта выгрузки.

Разные ключи — одна история. С каким бы ключом ни пришёл запрос к /v1/usage — даже с ключом, у которого выставлен список разрешённых моделей, — в ответе будет общий журнал аккаунта, а не журнал конкретного ключа. Разделение по ключам ведите у себя по X-Zukko-Cost-Micros или заводите отдельные аккаунты.

Если отчётность нужна не только по деньгам, но и по производительности — временам ответа, обрывам, — эти метрики /v1/usage не отдаёт, там только факт списания. Для мониторинга задержек и ошибок стоит логировать это на своей стороне, привязывая записи к тем же ключам и проектам, по которым вы сами ведёте учёт расхода.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.