Кратко
Переплату зачисляют целиком, и счёт закрывается оплаченным — но сообщает об этом событием invoice.paid_over, а не invoice.paid. Обработчик, сверяющий тип с одной строкой, теряет платёж, который вам уже заплатили. Недостача упирается в допуск проекта: до 2 %, шагом в десятую долю процента, 0,1 % у нового проекта. Дальше счёт либо остаётся открытым на остаток, либо закрывается недоплаченным. Деньги, дошедшие до адреса мимо счёта, — третий случай: собственного события у него нет, а сигнал бывает один — invoice.paid_late, когда поздние деньги в монете счёта покрыли сумму.
Счёт выставлен на 120 USDT. На адрес пришло 118,4 — ровно на комиссию биржи меньше. Никто ничего не перепутал, перевод подтверждён, и отката у него не будет.
Что произойдёт дальше, решили до того, как покупатель открыл страницу оплаты. Переплату зачисляют целиком, и счёт при этом закрывается. У недостачи два разных конца, и какой из них достанется счёту, решает настройка проекта.
Биржа удерживает свою комиссию из отправляемой суммы, у сумм в токенах длинный хвост после запятой, а человек, который доплачивает за заказ руками, округляет до ровного.
Как выставить счёт — отдельное руководство, а все переходы состояний собраны в описании платёжного потока. Здесь про сумму: что она делает с балансом, с заказом и с потоком событий.
Почему оплаченный счёт не закрывает заказ?
Потому что обработчик сверял тип события с одной строкой.
Событие до вас доходит. Подписка идёт категорией целиком, отдельные имена внутри неё по одному не выбираются, поэтому эндпоинт, подписанный на категорию счетов, получает все девять её событий — invoice.paid_over в том числе. Доставка приезжает, обработчик сверяет тип с invoice.paid, совпадения не находит и кладёт её в стопку «нас не касается».
Ломается это тихо, по устройству. В дашборде счёт оплачен, на балансе деньги, заказ не отгружен, и нигде никакой ошибки не было. Всплывает через несколько дней вопросом покупателя, где его товар. Причём отбор идёт ровно по тем, кто заплатил вам больше, чем вы просили.
Ветвитесь по множеству, а не по равенству:
// Оба события закрывают счёт. Второе означает, что пришло больше.
if (in_array($type, ['invoice.paid', 'invoice.paid_over'], true)) {
$this->fulfilOrder($invoiceId);
}
И держите две суммы врозь в собственной записи. Запрошено и получено — разные числа, а излишек это разница между ними. Вытаскивать его потом из имени события не придётся.
Что происходит с суммой сверх счёта?
Ничего не придерживают.
Всё, что пришло, зачисляется целиком, вместе с излишком, и счёт закрывается оплаченным. Комиссия считается по тому же правилу, что на любом другом платеже: с того, что пришло, а не с того, что просили, — значит, излишек несёт её наравне с остальной суммой. Сам процент лежит на странице цен.
Расходятся эти два случая только в событии. Платёж ровно в сумму сообщает invoice.paid. Платёж выше сообщает invoice.paid_over, и это имя — вся разница, которую получает ваша система.
Отправить излишек назад — ваш собственный перевод. С баланса само ничего не уходит, и решение здесь коммерческое, а не техническое: хвост округления перевода не стоит, а человек, промахнувшийся на один ноль, напишет вам раньше, чем вы ему.
Насколько меньше — это ещё оплачено?
Ползунок «Допуск недоплаты» живёт в настройках проекта. Верхняя граница у него — 2 %, двигается он десятыми долями процента, а проект, созданный сегодня, стоит на первой из них, на 0,1 %. Сдвиньте его в ноль, и сверка станет строгой: платёж должен дотянуть до суммы счёта или её перекрыть. Отдельно на один счёт допуск не выставить — в запросе на создание такого поля нет, и каждый счёт наследует то, что стоит в проекте.
В пределах допуска счёт закрывается и сообщает invoice.paid, как любой другой оплаченный. Расчёт идёт по пришедшей сумме, недостающее никто не добавляет.
Одна деталь решает, что ещё можно поправить потом. Счёт судят по тому допуску, который стоял в проекте в момент его создания, и эта копия замораживается на счёте до конца его жизни. Сдвинули ползунок — изменили те счета, которые выставите дальше, а не те, что уже открыты у покупателей на странице оплаты.
Откуда вообще берётся недостача?
Сетевая комиссия — первый подозреваемый и неверный.
Газ в Ethereum платят в эфире — так прямо и написано в документации сети, — а списывают его с монетного баланса отправляющего кошелька. Сумма в токене едет целиком. Кто отправил 120 USDT со своего кошелька, тот отправил 120 USDT.
При выводе с биржи сумма может прийти за вычетом комиссии. Binance.US формулирует правило про собственные выводы без обиняков: и комиссия площадки, и комиссия сети вычитаются из суммы вывода до того, как она дойдёт до кошелька назначения. Если плательщик указал сумму счёта, а биржа удержала из неё комиссию, счёт недополучит эту комиссию.
Вот почему допуск — процент, а причина — не процент. Фиксированное удержание откусывает заметную долю от маленького счёта и незаметную от большого, так что одним числом обе стороны не закрыть. На 0,1 % счёт в 120 единиц проглатывает 0,12 — хвост округления, и ничего похожего на биржевую комиссию. Ставьте допуск под хвост, а биржевой случай решайте тем, чем он и решается: предупреждением на странице оплаты или в подтверждении заказа.
Два конца у недоплаты — какой достанется счёту?
Развилку выбирает не поддержка и не покупатель, а поле allow_multiple_payments. Оно фиксируется при создании счёта, и API по умолчанию ставит true.
Если разрешены несколько платежей, недостача вне допуска оставляет счёт открытым. Счёт уходит в underpaid_waiting и остаётся открытым на остаток, а арифметику за плательщика делает страница оплаты: QR-код и все ссылки в кошельки перестраиваются на недостающую сумму, и считает её сервер, а не браузер. В карточке Telegram кнопка копирования суммы переименовывается и копирует остаток вместо исходного итога.
Если несколько платежей запрещены, недостача вне допуска закрывает счёт. Счёт закрывается недоплаченным и сообщает invoice.underpaid.
Открытый счёт добирает таймер. Окно оплаты закрылось, а счёт так и не дотянул — значит, он становится недоплаченным. Замеченный перевод, который ещё подтверждается, этому мешает напрямую: счёт не истечёт из-под денег, уже идущих к нему.
Платит ли вам недоплаченный счёт?
Платит, и на выписке это выглядит странно.
Каждый перевод зачисляется, как только подтвердился, и комиссия берётся с суммы этого самого перевода. Значит, частичная оплата — это деньги на балансе при всё ещё открытом счёте, а счёт, закончившийся недоплаченным, оставляет вас с настоящими деньгами на балансе и незакрытым заказом в системе.
Поток событий тише, чем движение денег. invoice.underpaid_waiting уходит один раз, когда счёт входит в это состояние. Второй перевод, после которого недостача всё ещё превышает допуск, двигает баланс и нового события не рождает: состояние не изменилось, а вебхук встаёт в очередь именно на смену состояния. Экран поддержки, собранный на потоке событий, покажет один приход там, где их было два. Текущую сумму читайте по счёту.
Остаётся решение, которое за вас никто не примет: отгрузить, отгрузить часть, попросить разницу или вернуть деньги. Возврат — обычный перевод с вашего баланса, ровно как в случае с переплатой.
Когда деньги на балансе — вообще не оплата счёта?
Счёт может недобрать. А может прийти перевод, который не оплатил ни одного счёта, — и по балансу их не отличить: он растёт в обоих случаях.
Природа у них разная. Недоплата — состояние счёта: у неё есть статус, который интеграция читает, и событие, на которое она подписывается. Зачисление мимо счёта — движение денег без счёта за ним: перевод дошёл до адреса после закрытия окна оплаты, или в другом активе, или на адрес, за которым счёта уже нет. Зачисляется он за вычетом обычной комиссии, а статус счёта от него не меняется: ни оплаченным, ни недоплаченным счёт не помечают.
Собственного события у зачисления нет. Поэтому система, живущая на потоке вебхуков, о самом приходе не узнает: показывают его на странице баланса, в письме, в колокольчике дашборда и в Telegram, если контакт привязан. До сервера доходит только один случай. Счёт истёк или закрылся недоплаченным, а поздние деньги в его монете, сами или вместе с прежними, покрыли сумму, — тогда счёт записывает paid_late_at и один раз отправляет invoice.paid_late, статус при этом не меняется. Частичный поздний платёж, другая монета или адрес без счёта не дают ничего.
Ошибка, которая из этого растёт, всегда одна и та же. Оператор видит приход, принимает его за недоплату и догружает заказ руками. Заказ уходит, счёт по-прежнему не оплачен, и в следующей сверке эта пара разъедется. Недобравший счёт надо догонять. Деньги без счёта просто разносят по учёту, и заказа за ними нет.
Как отрепетировать это до первого покупателя?
В тестовом режиме, симулятором оплаты: он поднимает те же события жизненного цикла, что настоящий платёж. Один подписанный вызов доводит подтверждённый тестовый счёт до нужного исхода:
POST /v1/sandbox/invoices/inv_7Qk2.../simulate-payment
Content-Type: application/json
{ "stage": "overpaid" }
paid оплачивает оставшуюся сумму, а overpaid — остаток со случайной положительной переплатой. underpay оплачивает случайную положительную часть остатка. Если разрешены несколько платежей, вызов underpay можно повторить: платежи складываются. Иначе инвойс закрывается со статусом underpaid. Суммы рассчитывает сервер; в запросе вы указываете только этап.
До первого настоящего платежа стоит прогнать три сценария:
overpaidпо счёту, за которым следит ваша отгрузка. Заказ обязан уйти, а в вашей записи должно быть видно, что получено больше, чем запрошено.underpayпо счёту, который допускает несколько платежей. Заказ уходить не должен, а то, во что смотрит поддержка, обязано показывать остаток, а не исходный итог.underpayпо счёту, созданному сallow_multiple_paymentsвfalse. Заказ не уходит, счёт закрывается.
Одну ветку так не проверить, и это как раз допуск. Этапа, который отправит на долю процента меньше, здесь нет, поэтому настройку сверяют глазами в проекте, а не выстрелом из теста. Всё остальное — один подписанный запрос.
| Что пришло | Что делает счёт | Какое событие придёт |
|---|---|---|
| Больше, чем просил счёт | Закрывается оплаченным, излишек зачислен | invoice.paid_over |
| Меньше, но в пределах допуска проекта | Закрывается оплаченным | invoice.paid |
| Недостача вне допуска, счёт принимает несколько платежей | Остаётся открытым на остаток | invoice.underpaid_waiting |
| Недостача вне допуска, счёт рассчитан на один платёж | Закрывается недоплаченным | invoice.underpaid |
| Остаток не пришёл до конца окна оплаты | Закрывается недоплаченным | invoice.underpaid |
| Тот же адрес, живого счёта за ним нет | Не двигается | Никакого; invoice.paid_late, если поздние деньги в его монете добрали сумму |
Частые вопросы
Что будет, если покупатель заплатил больше, чем в счёте?
Вся пришедшая сумма зачисляется на баланс, вместе с излишком, и счёт закрывается оплаченным. Ничего не удерживают и само собой ничего не возвращают. В вебхуке при этом приходит invoice.paid_over, а не invoice.paid.
invoice.paid_over — отдельное событие от invoice.paid?
Да, и на этом чаще всего спотыкаются интеграции. Эндпоинт, подписанный на категорию счетов, получает оба. Но обработчик, сравнивающий тип с одной строкой invoice.paid, переплату пропускает, и заказ остаётся неотгруженным.
Что такое допуск недоплаты и где он настраивается?
Это доля счёта, на которую платёж может не дотянуть и всё равно его закрыть. Настройка живёт в проекте, её потолок — 2 %, шаг — десятая доля процента, а новый проект стоит на 0,1 %. Ноль означает строгую сверку: платёж должен дотянуть до суммы счёта или её перекрыть.
Может ли покупатель доплатить по тому же счёту?
Если счёт создан с allow_multiple_payments true — это значение API по умолчанию, — недостача вне допуска оставляет его открытым в состоянии underpaid_waiting, и следующие переводы идут в тот же счёт, пока не закроется окно оплаты.
Зачисляется ли на баланс недоплаченный счёт?
Да. Каждый перевод зачисляется, как только подтвердился, и комиссия берётся с суммы этого перевода. Поэтому недобравший счёт оставляет вас с настоящими деньгами против заказа, который так и не закрылся.
Что происходит с переводом, пришедшим после закрытия окна оплаты?
Счёт он заново не открывает. Сумма ложится на баланс за вычетом обычной комиссии, статус счёта не меняется, а у самого зачисления вебхука нет — искать его нужно на странице баланса и в письме. Если поздние деньги в монете счёта, сами или вместе с прежними переводами, покрыли сумму, интеграция получит один invoice.paid_late. Частичный поздний платёж и другая монета вебхука не дают.
Когда НЕ стоит использовать допуск недоплаты
- Если по каждому счёту нужна полная сумма, ставьте допуск в ноль и сверяйте строго. Из собственного кошелька приходит сумма, которую отправил плательщик; допуск выше нуля позволяет закрыть счёт с недостачей.
- Если маржа заказа тоньше допуска, который вы рассматриваете, это снижение цены для каждого, кто округлит вниз. Пусть уступка лежит там, где её видно, — в цене, а не в настройке сверки.
- Если недостача, которую вы видите раз за разом, — фиксированное удержание биржи, процент не покроет её ровно. На больших счетах закроет, на маленьких промахнётся. Тут помогает разговор с плательщиком, а не ползунок.
- Если вы продаёте одну неделимую вещь — лицензию, место, билет, — счёт, ждущий второго платежа, чаще всего порождает наполовину оплаченные заказы, которые потом догоняет поддержка. Такие счета создавайте с выключенными несколькими платежами.
Источники
- 1. Документация Paymos — платёжный поток (accessed 2026-09-15)
- 2. Документация Paymos — симуляция оплаты инвойса (accessed 2026-09-15)
- 3. ethereum.org — Gas and fees (accessed 2026-09-15)
- 4. Binance.US Help Center — Understanding network fees vs. exchange fees (accessed 2026-09-15)
Последняя проверка: 14 окт. 2026 г.


