Сбои API, возврат кредитов и сверка: как проверить чистое списание за запрос
Разделяйте задачи, попытки вызова, состояния доставки и события кошелька, чтобы без двойного учёта обрабатывать отказы до списания, пустые результаты и частичный возврат кредитов; офлайн-примеры построены на целых числах.
Чтобы обещание «за сбои плата не взимается» было проверяемым обязательством продукта, нужно прояснить две вещи: что считается сбоем и как доказать отсутствие чистого списания. Если опираться только на коды HTTP-статуса, можно пропустить пустые результаты и частичную доставку, а если смотреть только на изменения баланса, в расчёт могут попасть параллельные запросы, пополнения баланса и корректировки. Для надёжной сверки нужно отслеживать отдельный запрос вместе со связанными с ним событиями.
В этой заметке описано, как клиентам API организовать записи для сверки, а неоднозначные поля поясняются на примере текущей кодовой базы EveryInfra. Показанный офлайн-реестр является примером проектирования приложения, а не выпиской по счёту клиента и не правилом признания выручки в бухгалтерском учёте. Конкретные условия оплаты для действий, денежные единицы и итоговые выписки нужно сверять с правилами тарификации соответствующего продукта.
Сначала разделите четыре объекта
- Бизнес-задача: цель, которой пользователь действительно хочет достичь, например изучить отзывы о наборе одобренных товаров. Одна задача может включать несколько вызовов API.
- Попытка вызова: фактический запрос, для которого сохраняются собственная временная метка, версия параметров, модель или действие, а также идентификатор запроса, если он доступен.
- Результат доставки: успех, пустой результат, частичная доставка, сбой или пока неизвестно; это бизнес-состояние, а не состояние кошелька.
- Событие кошелька: списание, возврат средств или другая явная корректировка, связанная с попыткой и позволяющая объяснить итоговое изменение баланса.
Свяжите эти четыре объекта между собой, а не втискивайте их в одно поле success. Если запрос выполнил часть цели, бизнес может оставить доставленный контент и продолжить проверку списаний по невыполненной части; при тайм-ауте доставка и расчёт тоже могут остаться неопределёнными, поэтому его нельзя просто записать как нулевые затраты.
Идентификатор бизнес-задачи, идентификатор запроса, асинхронный job_id и идентификатор операции в биллинге также являются разными понятиями. Храните только те связи, которые действительно предоставляет сервис; не создавайте несуществующие идентификаторы биллинга из похожих строк. Идентификаторы HTTP-запросов, созданные при запросе статуса задач, никогда не должны перезаписывать идентификатор связи с исходной отправкой задачи.
Отказ до списания и возврат после списания проходят по разным путям
Согласно развёрнутой базовой версии исходного кода на 2026-09-04, проверки учётной записи, входных данных, модели и прав на возможности для текстовых запросов EveryInfra выполняются до списания; при отсутствии messages ошибка входных данных может вернуться ещё до аутентификации, поэтому единый порядок приоритета для всех ошибок здесь не заявляется. Если messages корректны и проверка учётной записи пройдена, неизвестная модель приводит к 422 unknown_model до списания. Такие сбои являются отказом в выполнении и не порождают списание с последующим возвратом; прежнее описание с ответом 503 для неизвестной модели больше использовать не следует.
Для запросов, которые перешли к выполнению и затем завершились сбоем, может потребоваться возврат части списанных средств. Наличие ветки возврата в реализации не означает, что клиент увидел его завершение. Если ответ потерян, сначала запросите доступные записи по идентификаторам запроса; при отсутствии доказательств оставьте статус «расчёт ожидает проверки» и не меняйте финансовые записи только на основании текста ошибки.
В архивах за 2026-09-02 есть обезличенные образцы возвратов после сбоев, а также образцы пустых результатов и частичной доставки в пакетных запросах. Они показывают, что такие сценарии заслуживают отдельной приёмки, но не являются универсальной гарантией для каждой возможности или типа сбоя на сегодняшний день. В этой проверке рассматриваются только исходный код и публичная документация, без реальных списаний, возвратов средств и операций с аккаунтами.
Прочитайте определения полей, прежде чем решать, как их суммировать
На примере синхронных ответов с данными: billing.charged показывает статус списания в ответе, billing.credits содержит количество единиц тарификации, указанное в ответе, а billing.amount используется для отображения там, где нужна денежная семантика. Остаток квоты является снимком состояния аккаунта, и его нельзя считать стоимостью одного запроса; прейскурантные цены из каталога не заменяют фактические записи о запросах.
Частичные возвраты чаще всего приводят к ошибкам в расчётах. Текущая реализация для данных вычитает рассчитанную сумму возврата из исходной стоимости и записывает остаток в billing.credits, а refunded_credits возвращает отдельно. Поэтому вычитать refunded_credits из billing.credits ещё раз нельзя, иначе один и тот же возврат будет учтён дважды. Этот вывод относится только к проверенной здесь синхронной ветке данных и не распространяется автоматически на все продукты.
В полностью синтетическом примере при исходном списании 30 единиц и возврате 10 единиц чистое списание составляет 20. Если в итоговом ответе значение credits уже равно 20, а refunded_credits — 10, то 20 уже является указанной в этом ответе чистой суммой. Для проверки исходных событий считайте 30 минус 10; не вычитайте 10 из 20, чтобы получить 10.
Каждая попытка опроса по одному и тому же запросу может возвращать повторяющийся статус, поэтому не суммируйте кредиты по всем ответам. Храните снимки ответов, события биллинга и корректировки расчётов в базе данных раздельно, сначала дедуплицируйте их по соответствующим стабильным идентификаторам, а затем агрегируйте. Новая запись в HTTP-логе не означает, что произошло новое списание.
Интерпретируйте пустые результаты и частичную доставку с учётом действия
Если результаты поиска пусты, проверьте, не могут ли корректные результаты этого инструмента находиться в других полях. Текущая реализация общего поиска проверяет одновременно results и answer: если учитывать только пустое поле results, ответ с заполненным полем answer можно ошибочно признать полностью пустым. Клиенты должны следовать документированному формату ответа инструмента и не пытаться определять ценность содержимого на уровне финансового учёта.
Для пакетных запросов данных нужно сверять количество целей с количеством фактически полученных результатов. Текущая ветка частичного возврата средств возвращает total_targets, billed_targets и refunded_credits, а бизнес-результаты могут также содержать partial и missed. Эти поля помогают объяснить расхождения, но конкретные значения нужно брать из фактических ответов; количество комментариев нельзя считать количеством успешно обработанных целей.
Прежде чем повторно обрабатывать незавершённые цели, сохраните уже доставленные объекты и исходные запросы и определите, какие цели остаются в неизвестном состоянии. Повторная отправка всего пакета может продублировать уже полученные данные или создать новые попытки вызова. Дедупликация результатов на стороне клиента и защита от повторного списания на стороне сервера — это две разные операции.
Асинхронный приём задачи не означает окончательного расчёта
HTTP 202 и job_id означают, что задача поступила в конвейер обработки, а не что пользователь получил результаты. Сохраните исходную задачу и запросите её итоговый статус. Успешный запрос статуса означает лишь, что выполнен сам запрос: нужно проверить, выполняется ли задача или ожидает завершения. Обрыв соединения нельзя автоматически считать успешной отменой.
Текущие запросы статуса задач сбора данных в основном возвращают статус задачи, результаты или информацию об ошибке и не всегда содержат полный снимок биллинга. Если полей биллинга нет, сверяйтесь с авторизованными разделами биллинга; не подставляйте нули и не считайте снимки при создании и завершении отдельными новыми списаниями. В этой заметке не утверждается, что у всех асинхронных возможностей одинаковый момент расчёта.
Сверяйте стоимость отдельных запросов в целых единицах, а не в десятичных дробях, полученных с помощью вычислений с плавающей точкой
Для сверки сначала определите кошельки, единицы измерения и охват запросов. Не суммируйте напрямую суммы, относящиеся к разным кошелькам или валютам либо рассчитанные по отображаемым обменным курсам. Если интерфейсы используют целочисленные кредиты, прежде всего проводите проверку в этих исходных единицах; на границах системы проверяйте, что значения являются безопасными целыми числами, а для значений за пределами диапазона точных целых чисел JavaScript используйте явное целочисленное представление.
Ниже показана пользовательская структура событий: каждая запись относится к одному и тому же подтверждённому охвату запросов, значения credits представлены строками с положительными целыми числами, а kind задаёт тип события: списание или возврат средств. Пример не читает реальные записи биллинга, не подключается к базам данных и не меняет балансы. Вызывающая сторона должна заранее подтвердить полноту набора событий и наличие прав доступа к биллингу.
function reconcileAttempt(entries) {
const seen = new Map();
let debited = 0n, refunded = 0n;
for (const e of entries) {
if (typeof e.id !== "string" || !e.id.trim()
|| !["debit", "refund"].includes(e.kind)
|| typeof e.credits !== "string" || !/^[1-9][0-9]*$/.test(e.credits)) {
throw new Error("invalid synthetic ledger entry");
}
const fingerprint = JSON.stringify([e.kind, e.credits]);
if (seen.has(e.id)) {
if (seen.get(e.id) !== fingerprint) throw new Error("conflicting entry");
continue; // duplicate snapshots of the same event are not counted again.
}
seen.set(e.id, fingerprint);
if (e.kind === "debit") debited += BigInt(e.credits);
else refunded += BigInt(e.credits);
}
if (refunded > debited) throw new Error("check scope or missing debit evidence");
return {
debited: debited.toString(), refunded: refunded.toString(),
netCharged: (debited - refunded).toString()
};
}
console.log(reconcileAttempt([
{id: "synthetic-debit", kind: "debit", credits: "30"},
{id: "synthetic-refund", kind: "refund", credits: "10"},
{id: "synthetic-refund", kind: "refund", credits: "10"}
]));Результат: общая сумма списаний — 30, возвратов — 10, а чистое списание — 20; дублирующиеся события возврата не учитываются повторно. Если одному ID события соответствует противоречивое содержимое, пример останавливается, а не молча перезаписывает данные. Проверка также останавливается, если сумма возвратов превышает сумму зафиксированных списаний. Это может быть связано с неполным охватом данных или неполным временным окном и не означает автоматически, что сервис вернул слишком много средств.
Это не полный алгоритм кошелька. Пополнения, начисления, выводы средств, заморозки, сторнирования и другие корректировки в примере не моделируются; в реальных системах их нужно обрабатывать по определениям соответствующих событий. Нули, полученные из пустых списков, означают лишь отсутствие входных событий и не доказывают, что по неизвестному запросу не было списаний.
Числа типа Number в JavaScript не поддерживают целые значения произвольной точности. Рекомендации RFC 8259 о диапазонах совместимости чисел в JSON объясняют, почему здесь целые числа хранятся строками и явно преобразуются в BigInt. Это лишь уменьшает ошибки представления при вычислениях и не восполняет отсутствующие события биллинга.
При проверке по временному окну сохраняйте события на начало периода и события, переходящие между периодами
Запрос может начаться в один день и завершиться на следующий; события возврата или сторнирования также могут проводиться позже исходного списания. Фильтрация по времени создания запроса и по времени события в кошельке даёт разные наборы. В отчётах указывайте, какая временная база используется, и сохраняйте данные на начало и конец периода, а также связи между периодами, чтобы не поднимать ложную тревогу из-за возвратов, для которых не видны списания предыдущего дня.
Балансовые тождества требуют учитывать все списания и зачисления в пределах рассматриваемого периода по кошельку, а не только два типа событий, рассмотренных здесь. Разница между текущим балансом и балансом до конкретного вызова может также включать параллельные задачи или другие корректировки; её можно использовать для перекрёстной проверки, но не как самостоятельное доказательство стоимости отдельного запроса.
Расход средств из кошелька, денежные пополнения клиентов, использование промокредитов и бухгалтерская выручка определяются по-разному. Эта заметка касается только сопоставления запросов с данными кошелька и не содержит выводов о признании выручки или налогах. Внешние финансовые показатели следует публиковать только после того, как ответственные специалисты по продукту и финансам уточнят их определения.
Для сравнения полезна документация Stripe об идемпотентных запросах: безопасные повторные попытки возможны только при явной поддержке механизма идемпотентности со стороны сервиса. Это не доказывает, что EveryInfra принимает такой же ключ идемпотентности. Примеры в этой статье дедуплицируют полученные записи по ID события, а не блокируют повторные списания на стороне сервера; эти два вида дедупликации нельзя смешивать.
Направляйте расхождения в очереди проверки, не меняя записи в учёте автоматически
- Есть записи о списании без связанной доставки: проверьте, не продолжается ли асинхронная обработка, сохранены ли результаты и не потеряны ли связи.
- Неуспешные результаты с неясным статусом биллинга: проверьте исходные запросы и последующие события, не присваивая им самостоятельно статус возврата средств или сторнирования.
- Противоречащие друг другу данные об одном и том же событии: сохраняйте исходные версии и время получения, не допускайте их незаметной перезаписи.
- Событие возврата отображается несколько раз: определите, не было ли одно и то же событие прочитано несколько раз; не учитывайте его повторно.
- Несогласованные валюты, единицы измерения или временные окна: прежде чем оценивать расхождения, согласуйте границы сравнения.
В обращениях в поддержку указывайте только необходимые ID запросов, временные метки, возможности, статусы и обезличенные идентификаторы биллинга. Не отправляйте полные API-ключи, полное содержимое тел запросов клиентов или полные выгрузки аккаунта. Разбор расхождений и изменение записей в учёте требуют отдельных прав; автоматически созданная пометка об аномалии не даёт права на сторнирование или другие бухгалтерские корректировки.
По проверяемому запросу должно быть понятно, что было доставлено, какая база стоимости применялась, учтены ли возвраты в итоговой сумме и какие события ещё не подтверждены. Прояснение этих связей превращает принцип «за неудачные запросы итоговое списание не производится» из лозунга в проверяемое правило и предотвращает новые расхождения в биллинге из-за повторного вычитания возвратов на стороне клиента.
Справочник LF-14