Чтобы связать криптоплатёж с правильным заказом, сначала создайте заказ в своей системе и назначьте ему стабильный внутренний ID. Затем создайте отдельный payment invoice и передайте в него этот ID как контекст продавца. Сохраните invoice ID рядом с заказом. Когда доверенный серверный статус или подписанный webhook сообщит об оплате, найдите заказ по возвращённому reference, проверьте статус invoice, сохраните токен, сеть, сумму и tx hash и выполните заказ идемпотентно. Надёжная цепочка выглядит так: merchant order ID → payment invoice ID → confirmed blockchain transaction.
| Запись | Что она подтверждает | Чего она не подтверждает сама по себе |
|---|---|---|
| Заказ продавца | Кто, за что и сколько должен, что означает исполнение | Что средства поступили в блокчейне |
| Payment invoice | Какой токен, маршрут, сумму и срок ожидала система | Что продавец выполнил собственное бизнес-действие |
| Blockchain-транзакция | Публичный факт движения токена | Какой коммерческий заказ имел в виду продавец |
| Подписанное событие | Что платёжный сервис связал invoice со статусом и транзакцией | Что downstream worker применил изменение только один раз |
Blockchain-перевод не содержит ваш заказ
Публичный блокчейн записывает движение активов. Для ERC-20 стандартное событие Transfer содержит отправителя, получателя и значение, а receipt позволяет найти транзакцию. В стандарте ERC-20 нет номера ecommerce-заказа, аккаунта клиента, SKU или правила выдачи товара. Эти данные принадлежат системе продавца.
В этом различии находится основная проблема сопоставления. Поступление 100 USDC отвечает на вопрос «был ли перевод токена», но не сообщает, относится ли он к ORDER-4821, к поздней оплате ORDER-4710 или вообще к другой операции. Explorer показывает blockchain evidence, но не заменяет базу заказов.
Связь нужно создать до отправки checkout покупателю. Если после поступления средств пытаться угадать заказ по адресу, сумме или времени, каждый неоднозначный случай превращается в ручную работу поддержки.
Не используйте адрес или сумму как уникальный ключ
Общий адрес получения не уникален для заказа. Два клиента могут почти одновременно заплатить на один кошелёк. Один покупатель может повторно использовать старые реквизиты. Несколько EVM-сетей отображают адреса одинакового формата, хотя транзакции происходят в разных блокчейнах.
Сумма тоже не является безопасным join key. У нескольких заказов может быть одинаковая цена, клиент может недоплатить, переплатить или округлить значение. Необычные дробные суммы снижают число совпадений при небольшом объёме, но не заменяют явную связь заказа и invoice и не решают исключения.
Если проверенная сумма отличается от инвойса, используйте сценарий для недоплаты и переплаты и не запускайте штатное автоматическое исполнение заказа.
Время — дополнительное доказательство, а не идентичность. Задержка сети, пакетный вывод биржи или опоздавший клиент сдвигают транзакцию за ожидаемое окно. Адрес отправителя также нельзя считать постоянным ID клиента: биржа может выводить средства с общего инфраструктурного кошелька, а человек — пользоваться несколькими адресами.
Скриншот и присланный покупателем hash должны запускать проверку, но не закрывать заказ. Настоящий hash может относиться к другому токену, адресу, amount или network, а один hash можно предъявить повторно. Чек-лист ручной проверки показывает, как проверить сам перевод.
Храните три записи и явные внешние ключи
Разделите заказ продавца, payment invoice и blockchain transaction — у них разные зоны ответственности.
| Поле | Где хранить | Зачем оно нужно |
|---|---|---|
| Local order ID | База продавца и payment context | Стабильный ключ поиска и fulfilment |
| Payment invoice ID | Заказ продавца и провайдер | Прямой lookup и ссылка для поддержки |
| Сумма обязательства | Заказ продавца | Что клиент должен коммерчески |
| Payable token amount | Payment invoice | Точная сумма, показанная в checkout |
| Token и network | Invoice и финальная payment record | Не даёт смешать разные маршруты |
| Payment status | Платёжная запись и история | Определяет разрешённое действие |
| Transaction hash | Финальная payment record | Находит публичное доказательство |
| Paid и expiry timestamps | Платёжная запись | Объясняют timing и решения по исключениям |
| Event или delivery key | Webhook inbox | Предотвращает повторные side effects |
| Ручное решение и reviewer | Exception log | Делает override восстанавливаемым |
Используйте неизменяемый database ID или UUID. Номер, который сотрудник может отредактировать или выдать повторно, не подходит. Не помещайте в metadata секреты, private key и лишние персональные данные: контекст платежа может появляться в логах, кабинете и webhook body.
Invoice ID нужно записать в заказ сразу после создания — до redirect клиента. Тогда поддержка сможет пройти в обе стороны: от заказа к payment request и от invoice провайдера к бизнес-записи.
Создавайте invoice повторобезопасно
Нормальный flow сайта:
- Проверить корзину или заявку.
- Создать локальный заказ в состоянии
payment_pending. - Сформировать стабильный idempotency key для этого заказа и попытки оплаты.
- Создать payment invoice и передать local order ID как merchant context.
- Сохранить invoice ID и checkout URL в одной транзакции базы или в восстанавливаемом workflow.
- Отправить покупателя в hosted checkout.
Идемпотентность при создании нужна из-за неоднозначных сетевых ошибок. Провайдер мог создать invoice, даже если сайт не получил ответ из-за timeout. Повтор без того же ключа создаст второй активный запрос. Повтор со стабильным ключом должен вернуть исходный результат или явно показать конфликт.
В GramPayBot reference продавца передаётся в payload, а Idempotency-Key предотвращает дублирование invoice. Идентичный повтор возвращает исходный invoice; тот же ключ с другими параметрами приводит к конфликту. Сохраните public_id, а покупателю передайте web_app_invoice_url. Точный контракт описан в API quickstart и API reference.
Один заказ может иметь несколько последовательных payment attempts. Старый invoice может истечь, сумма — измениться, а покупатель — запросить другой маршрут:
order → payment attempt 1 (expired) → payment attempt 2 (paid)
Только одна попытка должна автоматически закрыть заказ. Старые попытки сохраняйте для аудита и разбора поздних переводов, а не перезаписывайте.
Финальный переход заказа должен принадлежать серверу
Браузер не является доверенным платёжным каналом. Клиент может закрыть страницу до подтверждения или повторно открыть старый success state. Нельзя выдавать продукт только потому, что в URL появился параметр успеха.
Используйте авторизованный API lookup или подписанный server-to-server webhook. В событии GramPayBot payload.payload содержит merchant reference, а invoice object — public_id, status, paid token, network и tx_hash. Формат определён в документации событий.
Безопасный webhook handler работает в таком порядке:
- Прочитать точный raw request body.
- Проверить HMAC-подпись сравнением constant time.
- Сохранить delivery и body до долгой бизнес-обработки.
- Дедуплицировать по стабильному delivery или event key.
- Быстро подтвердить валидное событие.
- В асинхронном worker найти payment attempt и order по сохранённым ID.
- Применить только разрешённый переход состояния.
- По возможности атомарно записать payment и fulfilment marker.
GramPayBot повторяет реальные события до 17 раз примерно в течение четырёх дней, поэтому дубликат доставки — ожидаемый механизм надёжности. Это общая практика: официальная документация Stripe также рекомендует сохранять обработанные event IDs и игнорировать дубли.
Сделайте идемпотентным само исполнение
Дедупликации delivery ID недостаточно. Два разных события либо webhook и polling worker могут одновременно увидеть оплаченный invoice. Само бизнес-действие нужно защитить unique constraint или условным state transition.
Worker может менять заказ только из payment_pending и создавать уникальный fulfilment record по order ID. Если не обновлена ни одна строка, другой процесс уже обработал оплату и повтор завершается без второй отправки или активации.
BEGIN
lock order ORDER-4821
if order.payment_state != payment_pending: stop safely
record paid invoice and tx_hash
set order.payment_state = paid
create unique fulfilment job for ORDER-4821
COMMIT
Цель — не exactly-once доставка webhook, которую нельзя гарантировать по всей цепочке. Практическая цель — at-least-once доставка с exactly-once бизнес-эффектом.
Свяжите статусы с разрешёнными действиями
| Payment state | Значение для продавца | Автоматическое действие |
|---|---|---|
active | Запрос действует, принятого подтверждённого платежа нет | Оставить заказ pending |
paid | Invoice выполнил правило подтверждения | Разрешить определённый идемпотентный fulfilment |
expired | Окно оплаты завершилось без принятого match | Заблокировать обычную выдачу, поздние переводы проверять отдельно |
cancelled | Запрос намеренно закрыт | Не принимать как текущую payment attempt |
Payment state и order state не обязаны называться одинаково. Оплаченный invoice может перевести заказ в ready_for_review, если нужны compliance или inventory checks. Истёкший invoice не обязательно отменяет коммерческий заказ — сайт может предложить новую попытку оплаты.
Храните историю статусов, а не только последнее значение. Поддержке может потребоваться понять, истёк ли invoice до поступления средств, кто разрешил manual override и какое событие запустило исполнение.
Отправляйте несовпадения в явные exception states
- Другой token contract: совпадение ticker не означает правильный платёж.
- Другая сеть: средства могут существовать на том же адресе в другом блокчейне, не совпадая с маршрутом invoice.
- Недоплата или переплата: храните expected и received amounts и применяйте письменную политику.
- Поздняя оплата: свяжите её с исторической попыткой, но не открывайте изменённый заказ автоматически.
- Повторный tx hash: не принимайте его как доказательство для второго invoice.
- Paid invoice без local order: сохраните событие, поднимите alert и не теряйте данные.
- Заказ уже исполнен: подтвердите retry без повторного side effect.
- Manual override: запишите сотрудника, причину и использованное доказательство.
Не стирайте фактическую транзакцию, чтобы записи выглядели удобнее. Оставьте исключение связанным с payment attempt, а решение о принятии, доплате или возврате передайте уполномоченному человеку. При direct-to-wallet зачислении исходящий refund контролирует продавец — monitoring service не может отменить on-chain перевод.
Отделяйте operational matching от бухгалтерской сверки
Operational matching отвечает на вопрос, можно ли сайту исполнить заказ. Accounting reconciliation шире: могут потребоваться fiat value на нужный момент, комиссии, конвертация, движение между кошельками, документы клиента и история возврата. Blockchain transaction не является автоматически налоговым invoice, договором или полной бухгалтерской записью.
Для ежедневного контроля сравнивайте оплаченные payment attempts с paid orders. Отмечайте записи только на одной стороне, повторные tx hashes и заказы, у которых fulfilment state расходится с payment state. Периодическая сверка находит проблемы после отключённого webhook, неудачного deployment или ручного изменения.
Hash нужен для публичного доказательства, но business join должен опираться на local order ID и payment invoice ID. Документация Ethereum показывает, что token transfers доступны через логи событий smart contract; эти логи содержат on-chain факты, но не коммерческий смысл продавца.
Пример с GramPayBot
Клиент выбирает тариф Pro на 30 дней:
- SaaS создаёт
ORDER-4821со статусомpayment_pending. - Backend вызывает
createInvoiceс amount99.00,payload: "ORDER-4821"иIdempotency-Key: ORDER-4821:crypto:1. - GramPayBot возвращает
public_idи hosted checkout URL; SaaS сохраняет их до redirect. - Покупатель переводит показанную сумму USDC в Base.
- GramPayBot подтверждает подходящую транзакцию и отправляет подписанное событие
invoice_paidс order reference, статусом, токеном, сетью и hash. - Webhook endpoint проверяет подпись raw body, сохраняет delivery и возвращает
2xx. - Worker блокирует
ORDER-4821, убеждается, что заказ ещё pending, записывает payment и создаёт одну уникальную задачу активации. - Повторная доставка подтверждается, но не может включить тариф второй раз.
GramPayBot предоставляет invoice checkout, мониторинг поддерживаемых сетей и связанный с заказом платёжный результат. SaaS остаётся источником истины для продукта, клиента, периода доступа и fulfilment. Более широкий процесс раскрывает руководство по автоматической проверке платежей, а website use case показывает его место в продажах.
Проверьте ошибки до запуска
Проведите небольшой production pilot и протестируйте не только успешную оплату. Повторите создание invoice с тем же idempotency key. Доставьте валидный webhook дважды. Дайте одному invoice истечь. Отправьте неправильную сумму без автоматического исполнения. Сымитируйте падение worker после сохранения события, но до изменения заказа. Убедитесь, что reconciliation находит и безопасно повторяет незавершённую работу.
Интеграция готова, когда каждый оплаченный заказ можно проследить от коммерческого обязательства через один payment invoice к принятой транзакции, а каждый retry и mismatch либо не создаёт повторного действия, либо появляется как видимый review item.
Следующий шаг
Автоматизируйте проверку оплаты на сайте
Создавайте invoice для каждого заказа, отправляйте покупателя на hosted checkout и получайте результат, связанный с заказом.
Приём платежей на сайте →