Тот же процент, что показывает /usage внутри Claude Code.
Проверено: 2026-08-04, Claude Code 2.1.221, тариф Max.
Эндпоинт не документирован публично. Схема снята с живого ответа и с
клиента (функция fetchUtilization в бинаре). Она может измениться без
предупреждения — декодер написан так, чтобы пропажа поля давала честную ошибку,
а не молчаливый ноль, и чтобы приложение уходило на локальную оценку.
GET https://api.anthropic.com/api/oauth/usage
Authorization: Bearer <accessToken>
Content-Type: application/json
Beta-заголовки не нужны — проверены три варианта, 200 даёт уже голый
Bearer. Клиент Claude Code ходит сюда с таймаутом 5 с и флагом
refreshOAuth: true (на 401 обновляет токен и повторяет).
OAuth-креды Claude Code, запись Keychain Claude Code-credentials (generic
password). Читается она /usr/bin/security find-generic-password -w, а не
своим запросом к Keychain: у утилиты доступ к записи не отбирается при
обновлении токена, потому что ею же этот токен и пишется — разбор в
USAGE.md. Запасной путь —
SecItemCopyMatching по kSecAttrService, без права показывать диалог.
Своей авторизации у ClaudeWeek нет и не будет: сервер узнаёт аккаунт по этому
токену, поэтому виджет всегда показывает те же цифры, что /usage.
Другого токена этот эндпоинт не принимает — проверено 2026-08-05. Годовой
токен от claude setup-token (sk-ant-oat01-…, тот же префикс, что у токена
сеанса) получает 401 Invalid bearer token; тот же токен на
/api/oauth/profile — 401 Invalid OAuth token. The provided token was not found or is malformed; заголовки anthropic-beta: oauth-2025-04-20 и
anthropic-version ничего не меняют. Документация Claude Code это
подтверждает: такой токен умеет «only model requests». API-ключ
(sk-ant-api…) не подходит тем более — эндпоинт oauth-only.
Отсюда следствие для интерфейса: поля «свой токен» в приложении нет.
Оно было и вырезано вместе с authSource — вставлять туда нечего, кроме
чужого токена сеанса, который живёт около часа и не обновляется.
Своего OAuth-флоу тоже не будет. Для него нужен client_id, зарегистрированный
у Anthropic; сторонним программам его не выдают, а единственный подходящий
принадлежит Claude Code — тогда человек в окне согласия разрешал бы доступ не
тому, кто просит. Чтение Keychain честнее: разрешение спрашивает macOS
системным диалогом, и его можно не дать.
Форма записи:
{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-…",
"expiresAt": 1786000000000,
"refreshToken": "…",
"refreshTokenExpiresAt": 1786000000000,
"rateLimitTier": "…",
"scopes": ["…"],
"subscriptionType": "max"
},
"organizationUuid": "…"
}expiresAt — миллисекунды (значения меньше 1e11 трактуем как секунды).
Токен живёт около часа.
organizationUuid читается тоже — из него и subscriptionType складывается
метка аккаунта (OAuthCredentials.accountMark, вида 7f3a1b2c·max). Токен
меняется раз в час, а она держится, пока не вошли другим аккаунтом, — и это
единственное, чем смена аккаунта замечается вообще: в транскриптах
~/.claude/projects маркера аккаунта нет, а сервер отвечает по токену и о
смене не сообщает. По расхождению метки счёт начинается заново
(USAGE.md). Метка в state.json хранится целиком,
сам UUID — нет: сравнивать хватает её.
Обновлением токена ClaudeWeek не занимается. Refresh-цикл — дело Claude
Code; два процесса, наперегонки меняющие одну запись Keychain, теряют токен.
Мы перечитываем запись перед каждым запросом и на 401 уходим на локальную
оценку, а не пытаемся чинить авторизацию сами.
Значимые поля (остальные — null или не про нас):
| Поле | Тип | Смысл |
|---|---|---|
seven_day.utilization |
число 0…100 | расход недельного лимита — то, что показывает виджет |
seven_day.resets_at |
ISO 8601 | момент сброса недели — источник правды для окна |
five_hour.utilization |
число 0…100 | расход пятичасовой сессии — первая полоса панели |
five_hour.resets_at |
ISO 8601 | когда отпустит сессия; без него процент не показываем |
limits[] |
массив | те же лимиты списком: kind, percent, resets_at, is_active |
seven_day_opus, seven_day_sonnet, seven_day_oauth_apps |
объект или null |
лимиты по моделям; на Max приходят null |
extra_usage, spend |
объект | докупленные кредиты; к недельному лимиту отношения не имеют |
resets_at приходит с микросекундами и смещением вместо Z:
2026-08-07T12:00:00.357993+00:00. ISO8601DateFormatter разбирает такую
строку только с .withFractionalSeconds, а строку без долей — только без этого
флага, поэтому в ISO8601.parse держатся оба форматтера. Разбор доли секунды
сохраняет, а округляет их уже провайдер — ниже.
Живой ответ (2026-08-04, сокращён):
{
"five_hour": {"utilization": 41, "resets_at": "2026-08-04T20:19:59.357955+00:00",
"limit_dollars": null, "remaining_dollars": null, "used_dollars": null},
"seven_day": {"utilization": 50, "resets_at": "2026-08-07T12:00:00.357993+00:00",
"limit_dollars": null, "remaining_dollars": null, "used_dollars": null},
"seven_day_opus": null,
"seven_day_sonnet": null,
"limits": [
{"group": "session", "kind": "session", "percent": 41, "is_active": false,
"resets_at": "2026-08-04T20:19:59.357955+00:00", "severity": "normal"},
{"group": "weekly", "kind": "weekly_all", "percent": 50, "is_active": true,
"resets_at": "2026-08-07T12:00:00.357993+00:00", "severity": "normal"}
],
"member_dashboard_available": false
}Недельная строка читается сначала из seven_day, а если он null — из
limits[] по kind == "weekly_all". Оба представления в наблюдавшемся ответе
совпадают; дубль держим на случай, если одно из них пропадёт.
Если не нашлось ни то ни другое — UsageError.decoding, и приложение уходит на
локальную оценку. Ноль вместо процента не показываем никогда: «потрачено 0 %» —
опаснее честного «не смог».
| Ситуация | Поведение |
|---|---|
| подряд идущие обновления | не чаще раза в 60 с, между ними отдаётся значение из памяти |
401, 403 |
unauthorized → локальная оценка; пауза, затем перечитываем Keychain |
429, 5xx, сеть |
пауза 1 → 2 → 4 → 8 → 15 минут |
| во время паузы | отдаётся последнее удачное число, но не дольше 15 минут с момента, когда оно получено; дальше — честная ошибка и уход на локальную оценку |
| Mac спит | таймер гасится на willSleepNotification, будится на didWakeNotification |
Пауза после отказа не «до перезапуска», как предполагал первоначальный план: Claude Code обновляет токен сам, и следующая попытка через минуту обычно проходит. Перезапуск приложения ради этого — лишний.
Возраст числа виден всегда. Снимок помечается моментом, когда сервер
ответил, а не моментом, когда его спросили: внутри минутного троттлинга и
во время паузы после отказа наружу идёт одно и то же число, и выдавать его за
свежее нельзя. Старше двух минут — кружок источника желтеет и подписывается
«данные offline» (PanelModel.freshFor). Старше пятнадцати — число
отбрасывается совсем (OfficialProvider.staleLimit): локальная оценка по
сегодняшним транскриптам честнее позавчерашнего официального процента,
выданного за сегодняшний.
Единственный сетевой адресат — api.anthropic.com. Токен не логируется, не
пишется на диск и не выводится в UI. В кеш (~/.config/claude-week/cache.json)
кладутся только проценты, границы окна и метка времени.
resets_at округляется до минуты — у обоих лимитов. Сервер отдаёт его
с секундным дрейфом — в одном ответе 12:00:00.357993, в следующем
11:59:59.9…, — и без округления подпись прыгает между «сброс ПТ 16:00» и
«сброс ПТ 15:59», а обратный отсчёт сессии дёргается на минуту туда-сюда. Сброс
имеет минутное разрешение; полминуты на недельном окне не значат ничего.
Пятичасовая сессия кешируется вместе со своим resets_at. Локальный
источник её не считает вовсе — веса моделей приближают недельную формулу, а не
сессионную, и второе число по тем же данным было бы выдумкой. Поэтому офлайн
показывается последнее официальное значение, но только пока resets_at в
будущем: после сброса процент не «слегка устарел», а обнулился, так что из
кеша он стирается, а строка сессии из панели исчезает.
Официальный процент калибрует локальную оценку. На каждом успешном ответе
локальная стоимость недели делится на официальный процент, и полученный бюджет
(weeklyBudget) вместе с моментом сброса (officialWindowEnd) сохраняется в
cache.json. Когда сеть пропадает, офлайн-оценка опирается на них, а не на
resetHour из конфига. Замер: 52 % официальных против 52 % офлайн-оценки.
Калибровка идёт не ниже 5 % (ResolvingProvider.minimumCalibrationPercent).
Процент приходит целым числом: на 2 % ошибка округления — четверть значения, и
бюджет из неё вышел бы перекошенным вдвое. В начале недели держим прежний
бюджет и ждём, пока расход накопится.
seven_day_opus, seven_day_sonnet, seven_day_oauth_apps, extra_usage,
spend — лимиты по отдельным моделям и докупленные кредиты. На проверявшемся
тарифе Max все приходили null, поэтому разбор не написан. На других тарифах
там числа, и похожие программы их показывают: отдельной строкой под недельный
лимит модели и суммой в долларах под докупленное. Пункт 5
роудмапа; перед работой обязательно снять живой ответ зондом —
угадывать форму по одному пустому полю нельзя.
Разбивки по моделям. «Сколько ушло на Opus, а сколько на Sonnet» сервер не
сообщает: seven_day_opus и seven_day_sonnet — это отдельные лимиты по
моделям, а не расход, и на проверявшемся Max они приходят null; токенов и
долларов по моделям в ответе нет ни в каком виде. Поэтому разбивка в панели
(клик по цифрам процента ставит её на место строк дней) считается по локальным
транскриптам и взвешивается по ценам моделей — итог недели при этом остаётся
официальным, а доли помечены знаком ≈ у каждого числа и подписью под
строками: «примерный локальный подсчёт».
Разбивки по суткам. Приходит одно число на всю неделю. Форма недели
восстанавливается из локальных транскриптов и масштабируется к официальному
итогу (решение §2.3 плана): доля каждых суток в локальной стоимости умножается
на официальный процент. Итог поэтому точный, а полосы — правдоподобные.
В панели этой оговорки нет: кружок источника говорит только «данные online»
или «данные offline», а различие итога и формы живёт в --json
(isEstimate) и здесь.
swift scripts/probe-usage.swiftЗонд читает токен из Keychain, делает один запрос и печатает ответ целиком. Токен не выводит — только префикс и длину.