Экспорт счёта API не сходится: сверяем ID запроса, повторы и часовой пояс
Разделите детализацию запросов, сводный счёт и движение баланса, затем проверьте ID запроса, попытки, UTC и время проводки, чтобы найти повторы и расхождения суток; приведён локальный пример.
Если экспорт счёта не сходится, не пополняйте баланс повторно и не складывайте две строки без проверки. Посредник, шлюз и вышестоящий сервис могут использовать разные идентификаторы, часовые пояса, правила повторов и моменты расчёта. Ниже приведён проверяемый способ разложить расхождение на повторную попытку, границу суток и задержку проводки.
Сначала зафиксируйте три суммы
Для каждой сверки задайте три независимых показателя: стоимость в деталях запросов, сумма в сводке счётного периода и чистое изменение баланса или платёжного журнала. Они не обязаны обновляться одновременно и могут включать разные позиции. Изменение баланса дополнительно включает пополнения, возвраты, сгорание бонусов и комиссии.
Записывайте строку как request_id, attempt_id, created_at, settled_at, model, input_units, output_units, amount, currency, status и parent_request_id. Если сервис не выдаёт поле, оставьте его пустым и укажите «поле не предоставлено». Не подменяйте отсутствующий идентификатор локальным временем или придуманным значением. Сначала полезно прочитать о границах запросов и оплаты в API-посреднике, а затем добавить правила идентификаторов, повторов и времени из этой статьи.
Разделяйте запрос и попытку повтора
Один клик пользователя может создать одну бизнес-задачу и несколько сетевых попыток. У задачи обычно есть локальный job_id, у каждой отправки — attempt_id, а вышестоящий сервис может вернуть request_id. Не объединяйте эти понятия: вторая попытка той же задачи может завершиться успешно, пока первая уже стоит в очереди и тоже породила расход.
Группируйте в таком порядке: сначала по выданному сервисом request_id, затем по parent_request_id, и только потом по локальному job_id с временным окном. Временное окно является подсказкой, а не доказательством тождественности. Тайм-аут или отмена клиента также не означают автоматически нулевую стоимость. Границы отмены следует проверять вместе с уровнями передачи сигнала в статье отмена генерации и сверка счёта.
Приведите часовые пояса к UTC
Экспорт может показывать UTC, часовой пояс сервиса или настройку аккаунта. Не соединяйте таблицы по строке даты. Сохраните исходное время и его часовой пояс, преобразуйте момент в UTC, а затем вычислите отдельную финансовую дату, например beijing_date.
Например, 2026-10-29 00:05 по Пекину — это 2026-10-28 16:05 UTC. По строкам даты эти записи попали бы в разные дни, хотя момент один. Сервис также может делить расчётный день по собственной границе, поэтому «стоимость запросов за день» и «счёт за день» сравнимы только при одинаковом определении суток. Для запроса через полночь сохраняйте и created_at, и settled_at; время экспорта не заменяет время события.
Минимальная таблица сверки
Сохраните CSV или таблицу базы данных со следующими колонками:
| Поле | Назначение | Что нельзя выводить напрямую |
|---|---|---|
request_id | Связь с деталями сервиса | Это не гарантия единственного списания |
attempt_id | Разделение попыток отправки | Попытка не обязательно успешна |
created_at_utc | Вход запроса в систему | Это не время расчёта |
settled_at_utc | Момент проводки | Может быть пустым или отложенным |
amount, currency | Сумма и валюта | Не включает разницу обменного курса |
status | Успех, ошибка, отмена | Само по себе не доказывает оплату |
source | Детали, сводка или платёж | Источники могут обновляться с задержкой |
До конвертации валют храните исходную сумму и валюту. Бонус, скидку, возврат и комиссию отмечайте отдельным типом или знаком; не меняйте исходную строку ради совпадения итогов. Если баланс есть, но запрос отклоняется из-за лимита группы или ключа, используйте пошаговую проверку из статьи баланс есть, но квоты недостаточно.
Локальный скрипт для поиска повторов
Пример работает только с искусственным массивом и не обращается к платному API. Он группирует одинаковый request_id и помечает группы, требующие проверки связи между попытками:
function reconcile(rows) {
if (!Array.isArray(rows)) throw new TypeError('rows must be an array')
const groups = new Map()
for (const row of rows) {
if (!row || typeof row.request_id !== 'string' || !row.request_id) continue
const amount = Number(row.amount)
if (!Number.isFinite(amount)) continue
const group = groups.get(row.request_id) ?? { requestId: row.request_id, rows: [], total: 0 }
group.rows.push(row)
group.total += amount
groups.set(row.request_id, group)
}
return [...groups.values()].map(group => ({
...group,
duplicateCandidates: group.rows.length > 1,
total: Math.round(group.total * 1e8) / 1e8,
}))
}
const sample = [
{ request_id: 'r-100', attempt_id: 'a-1', amount: '0.012', status: 'timeout' },
{ request_id: 'r-100', attempt_id: 'a-2', amount: '0.012', status: 'succeeded' },
{ request_id: 'r-101', attempt_id: 'a-3', amount: '0.004', status: 'succeeded' },
]
console.log(reconcile(sample))
duplicateCandidates означает только «нужна ручная проверка», а не «первую оплату надо удалить». Если правила сервиса говорят, что неудачная попытка не оплачивается, подтвердите это строкой детализации или ответом поддержки, а не только кодом HTTP. Пример специально пропускает строку без идентификатора и нечисловую сумму, чтобы повреждённые данные не меняли вывод.
Разложите расхождение баланса по источникам
Для одного аккаунта, одной валюты и одной границы суток выгрузите четыре набора: детали запросов, сводку счёта, пополнения и возвраты, бонусы или скидки. Суммируйте их отдельно и проверьте рамочную формулу:
конечный баланс = начальный баланс + пополнения - возвраты - стоимость запросов + изменение бонусов - комиссии
Это способ организации сверки, а не универсальное правило конкретной площадки. Если позиция отсутствует, оставьте разницу «не объяснена» и сохраните время экспорта и фильтры. Когда одна задача встречается дважды, выясните, является ли одна строка резервированием, а другая окончательным расчётом, или это две отдельные сетевые попытки. Совпадение сумм само по себе не разрешает объединение. Для возврата храните отдельно уплаченную часть, бонус, использованную сумму и комиссию; полезна памятка по доказательствам для возврата баланса.
Сохраните доказательства и сформулируйте запрос
Храните исходный файл, условия фильтра, время экспорта, обезличенный идентификатор аккаунта и описание часового пояса. В поддержку отправляйте только необходимый request_id, сумму, валюту, время в UTC и статус. Не прикладывайте API Key, полный промпт или конфиденциальный ответ. В рабочую таблицу добавьте колонки «текст сервиса», «локальный вывод» и «нужно подтвердить», чтобы гипотеза не стала утверждением.
Если расхождение связано с бесплатным балансом, сроком действия или платным fallback, сначала прочитайте правила; проверка бесплатного лимита и перехода на оплату помогает разделить типы баланса. При смене домена или платёжной страницы сохраните версию условий и объявление миграции по статье условия, блокировка и выход.
После исправления повторите проверку на новом безопасном локальном сценарии, который можно отменить. Не создавайте реальный платный запрос ради «тестовой строки» и не переписывайте исходный экспорт, чтобы свести итог. Разницу можно пометить объяснённой только после проверки идентификаторов, временной шкалы, связи повторов, валюты и движения баланса.