История списаний через 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 там просто не будет.
Базовый запрос:
curl https://api.zukko.pro/v1/usage \
-H "Authorization: Bearer fetch_ваш_ключ"Курсорная пагинация означает, что вместо номеров страниц сервер отдаёт указатель на следующий кусок данных. Логика стандартная для такого подхода: получаете первую порцию, берёте из ответа курсор для продолжения, подставляете его в следующий запрос, повторяете, пока курсор не закончится.
curl "https://api.zukko.pro/v1/usage?cursor=NEXT_CURSOR" \
-H "Authorization: Bearer fetch_ваш_ключ"Скрипт для полной выгрузки
Для отчёта обычно нужна не одна страница, а весь период целиком. Питоновский скрипт, который проходит по всем страницам и складывает записи в список:
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, без сторонних библиотек:
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:
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.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.