payment_intents, а не с объектной моделью конкретного провайдера.
Объявление возможностей вместо особой обработки каждого провайдера — это то, что позволяет экрану
объяснить, почему что-то не предлагается, а не просто отказать. Тариф с ценой только в RUB не
покажет Telegram Stars, а у платежа через CryptoBot не будет кнопки возврата, потому что оба факта
приходят от самого адаптера.
Эта страница — руководство по настройке. Конечный автомат заказа, классификации расчётов и то, что
порождает оплаченный заказ, описаны в заказах и платежах.
Четыре адаптера
Как адаптер становится доступным
Адаптеры собираются из переменных окружения при старте процесса, и в API, и в воркере, поэтому у двух процессов не может оказаться разного набора провайдеров — иначе получился бы отказ, при котором воркер не может списать продление через того провайдера, которым платил клиент.manualиtelegram_starsрегистрируются всегда.cryptobotрегистрируется, когда заданAPP_CRYPTOBOT_TOKEN.yookassaрегистрируется, когда заданAPP_YOOKASSA_SHOP_IDилиAPP_YOOKASSA_SECRET, и затем отказывается собираться, если присутствуют не оба значения.
422 provider_unavailable.
Какие способы предлагаются клиенту
Способ оплаты появляется, только если установка его настроила и тариф имеет цену в валюте, в которой этот способ проводит расчёты. Выбор никогда не откатывается к несовместимому адаптеру, а заказу, который уже полностью покрыт кошельком, не предлагается ни один провайдер. У пополнений кошелька нет прайс-листа тарифа, поэтому они предлагают все включённые адаптеры, поддерживающие валюту. Список, который видит клиент, отсортирован по слагу провайдера —cryptobot, manual,
telegram_stars, yookassa, — и обе поверхности используют одну и ту же сортировку, поэтому способ
оплаты никогда не окажется на одной позиции в боте и на другой в браузере. Поле порядка отображения
в панели этого не меняет.
На
/admin/settings есть карточка Payment providers, которая хранит enabled, порядок
отображения, запечатанные учётные данные и секрет вебхука для каждой пары (provider, merchantId).
Ничто из этого не настраивает работающий адаптер: ConfigureProviders читает только переменные
окружения, а клиентский список использует порядок отображения, зашитый в код. У сохранённых учётных
данных нет ни одного читателя. Настраивайте провайдеров в .env и перезапускайте.
Настройки установки описывают, на что экраны настроек
влияют, а на что нет.Telegram Stars
Проводит расчёты в Telegram Stars — валюте без дробной части с кодомXTR — через аутентифицированный
Bot API. Это единственный канал, которому не нужен ни счёт мерчанта, ни внешнее подключение, и
единственный, чьи деньги полностью остаются внутри Telegram.
Почему путь расчёта здесь другой. Telegram не подписывает обновления Bot API для публичного
HTTP-маршрута. Принимать «вебхук Stars» на /v1/payments/webhooks/telegram_stars означало бы
принимать неаутентифицированные деньги, поэтому адаптер не объявляет возможность вебхуков, а
публичный маршрут отклоняет этого провайдера ещё до разбора чего-либо. Расчёт приходит только через
собственный аутентифицированный поток обновлений бота.
1
Задайте токен бота
APP_TELEGRAM_TOKEN — тот же токен, который использует процесс бота. В API он включает адаптер
Stars и вход через Telegram; в воркере — Stars для продлений. О том, как его получить, см.
Telegram.2
Назначьте версии тарифа цену в XTR
Тариф без цены в
XTR невозможно купить за Stars, потому что выбор провайдера сопоставляется с
валютами цен тарифа. APP_DEFAULT_CURRENCY=XTR делает установку, работающую только на Stars,
согласованной везде.3
Купите в боте
На экране заказа появляется кнопка счёта. Бот перечитывает заказ, отказывает, если валюта не
XTR или не осталось внешней суммы к оплате, и отправляет счёт Telegram с идентификатором заказа
в качестве полезной нагрузки и включённой защитой от пересылки.4
Следите за ответом на pre-checkout
Telegram присылает
pre_checkout_query. Omniflow подтверждает его, только если указанный заказ
принадлежит этому клиенту, валюта совпадает, итог в точности равен внешней сумме и заказ ещё
открыт. Всё остальное получает отказ и сообщение об устаревших данных, а не молчаливое принятие.successful_payment обновление записывается как событие провайдера с ключом
charge:<telegram_payment_charge_id>, из-за чего переотправленное обновление становится пустой
операцией с классификацией duplicate. В качестве сырого тела сохраняется собственная проекция
Omniflow — {updateId, orderId, chargeId, amountMinor, currency}, — а не обновление Telegram: ни
содержимого сообщения, ни данных аккаунта. Идентификатор списания записывается в намерение платежа
до расчёта, поэтому повторная попытка снова найдёт намерение, а у возврата будет нужная ссылка.
Списание, чьё намерение принадлежит другому заказу, отклоняется с order_mismatch.
Возвраты. refundStarPayment требует и идентификатор списания, и пользователя Telegram, который
платил. Плательщик сохраняется как несекретные метаданные чека, если платёж запущен с поверхности,
которая знает аккаунт Telegram. Возвраты возможны только на полную сумму списания: частичный запрос
отклоняется, а не сообщается как проведённый.
Тестирование. Песочницы нет. Назначьте дешёвой версии тарифа цену в XTR и купите её со своего
аккаунта Telegram; собственное тестовое окружение Telegram здесь не рассматривается.
CryptoBot
Создаёт крипто-счета, номинированные в фиате, черезpay.crypt.bot. Оператор назначает цену в
фиатной валюте, а клиент платит криптовалютой — именно поэтому это тот канал, который уже используют
большинство магазинов, живущих внутри Telegram.
Валюты: USD, EUR, RUB, BYN, UAH, GBP, CNY, KZT, UZS, GEL, TRY, AMD,
INR, AZN, AED, PLN. Telegram Stars намеренно отсутствуют — они никогда не проводятся через
CryptoBot.
1
Создайте приложение в @CryptoBot
Откройте @CryptoBot, перейдите в Crypto Pay, создайте приложение и
скопируйте его API-токен. CryptoBot держит отдельного бота для тестовой сети со своими токенами;
токен testnet работает, только пока
APP_CRYPTOBOT_TESTNET включён, а токен основной сети —
только пока он выключен.2
Настройте Omniflow
APP_CRYPTOBOT_TESTNET по умолчанию равен true, что направляет адаптер на
https://testnet-pay.crypt.bot/api. Для продакшена задайте false; если оставить значение по
умолчанию, настоящие счета не будут создаваться никогда.3
Направьте вебхук на Omniflow
В настройках приложения включите вебхуки и укажите URL
https://<your-public-host>/v1/payments/webhooks/cryptobot. Маршрут не аутентифицирован по
необходимости и защищён проверкой подписи, описанной ниже.4
Купите что-нибудь недорогое
В тестовой сети создайте заказ и оплатите счёт. Заказ должен рассчитаться за считанные секунды;
если этого не произошло, посмотрите запись о приёме в
/admin → System → Webhooks и помните, что
минутный сверщик всё равно опросит счёт.crypto-pay-api-signature декодируется из hex и сравнивается за
постоянное время с HMAC-SHA256(key = SHA256(apiToken), body) по точному сырому телу. Изменённое
тело отвергается. Идентификатор дедупликации — {updateType}:{invoiceID}:{updateID}, он стабилен
при повторах одного и того же тела.
Восстановление и опрос. Если CreateIntent повторяется после потерянного ответа, адаптер
запрашивает последние 100 счетов и принимает тот, чья полезная нагрузка соответствует заказу, — на
один заказ второго счёта не будет. Опрос использует getInvoices?invoice_ids=… и обслуживает как
фоновый сверщик, так и кнопку Обновить у клиента. Счета создаются с expires_in=3600.
YooKassa
Российский эквайер, работа черезhttps://api.yookassa.ru/v3 с HTTP Basic-аутентификацией. Это
единственный адаптер, поддерживающий частичные возвраты, и единственный, который заявляет поддержку
рекуррентных платежей.
1
Получите идентификатор магазина и секретный ключ
В личном кабинете YooKassa создайте секретный ключ для API. Вам нужны идентификатор магазина и
ключ вместе.
2
Настройте Omniflow
APP_PUBLIC_URL
обязателен всегда, когда настроена YooKassa, потому что размещённой платёжной странице нужна
абсолютная ссылка для возврата; бот отказывается стартовать без неё.3
Укажите URL уведомлений
В личном кабинете направьте HTTP-уведомления на
https://<your-public-host>/v1/payments/webhooks/yookassa.4
Оплатите тестовый заказ
Используйте тестовый магазин YooKassa и его тестовые карты. После подтверждения клиент попадает на
{APP_PUBLIC_URL}/account/orders/{orderID} — это возвратный URL, который Omniflow передаёт в
каждом создаваемом намерении платежа.POST /payments с capture: true, подтверждением типа redirect, несущим возвратный
URL, metadata.order_id и ключом идемпотентности Omniflow в заголовке Idempotence-Key. YooKassa
соблюдает этот ключ 24 часа — именно это делает безопасной повторную попытку воркера.
Проверка вебхука — самое важное. YooKassa не подписывает свои уведомления. Поэтому Omniflow
относится к уведомлению как к недоверенной подсказке: проверка лишь разбирает конверт и требует
наличия типа уведомления с непустым идентификатором объекта, после чего HandleWebhook немедленно
заново запрашивает указанный платёж через аутентифицированный API и заменяет статус и сумму из
уведомления опрошенными значениями, прежде чем изменится хоть какое-то финансовое состояние. Если
этот запрос не удался, строка вебхука завершается как failed с reconciliation_failed, и ничего не
применяется. Атакующий, способный отправлять запросы на маршрут вебхука, не может сделать платёж
успешным.
Идентификатор события вебхука — {event}:{objectID}:{first 8 bytes of sha256(body)}, именно он
дедуплицирует повторную доставку.
Возвраты. POST /refunds с идентификатором платежа, суммой и ключом идемпотентности вызывающей
стороны. Поддерживаются и полные, и частичные — YooKassa единственный адаптер, у которого частичный
возврат доходит до провайдера.
Рекуррентные платежи. ChargeSaved отправляет платёж с сохранённым payment_method_id и
capture: true за один шаг, потому что рядом нет никого, кто мог бы подтвердить двухфазную
авторизацию.
Ручные платежи
Адаптер без внешней системы.Create возвращает намерение платежа, у которого ссылка провайдера —
это идентификатор заказа, а статус — requires_action; опрашивать нечего и вебхук проверять нечем.
Расчёт происходит, когда его подтверждает оператор, — именно это делает адаптер пригодным для
банковских переводов, наличных и любых каналов, которые Omniflow не интегрирует.
Поскольку деньги перемещались вне системы, ответственность несёт само подтверждение: оно отклоняется
без указанного оператора, причины и корректного ключа идемпотентности.
manual_payment_approvals, уникально для каждого намерения платежа, поэтому
второе решение, противоречащее первому, отклоняется с сообщением «manual payment already has a
different final decision». Подтверждение проводит полную сумму намерения через тот же путь
расчёта, что используют все остальные провайдеры. Отклонение фиксирует решение и не проводит ничего.
Клиент видит передачу платежа в режим manual: панель сообщает, что подтверждать будет оператор, а
экран заказа в Telegram говорит то же самое, пока заказ в ожидании.
Приём вебхуков
Один публичный маршрут обслуживает каждый адаптер, объявляющий поддержку вебхуков:POST /v1/payments/webhooks/{provider}. Он не несёт учётных данных — подлинность обеспечивается
проверкой подписи адаптера, а не токеном в URL, — а тело ограничено 64 КиБ (413 payload_too_large
сверх этого).
Что происходит с запросом, по порядку:
- Отказ, если названный адаптер не зарегистрирован или не объявляет поддержку вебхуков. Именно это
закрывает маршрут для
telegram_starsиmanual. - Вычисляется дайджест тела и запускается проверка адаптера. Неудача всё равно записывается —
строкой с
signature_valid = falseи идентификатором событияinvalid:<digest>— и отвечает400 webhook_rejected. Отклонённый вебхук, не оставивший следа, был бы неотличим от того, который вообще не приходил. - Вставляется строка события. Если сохранённая строка уже обработана или проигнорирована, ответ —
{"status": "duplicate"}, и на этом всё. Это защита от дублей, привязанная к собственному идентификатору события провайдера, а не к какому-либо заголовку Omniflow. - Только для YooKassa — платёж заново запрашивается через аутентифицированный API.
- Проверенное, но неуспешное событие завершается как
ignored, а неfailed. Истёкший счёт — не ошибка. - Находится намерение платежа, расчёт проводится по общему идемпотентному пути, и возвращается
200с классификацией.
content-type и
user-agent. Сохранение заголовков авторизации или подписи означало бы хранение учётных данных
провайдера в базе. Строки удаляются через 30 дней после приёма.
Операторы читают историю приёма в /admin → System → Webhooks (system.read). Сырое тело там
никогда не возвращается: это собственная полезная нагрузка провайдера, в ней могут быть платёжные
данные, а для ответа на вопрос «пришло ли это и было ли принято» достаточно статуса.
Действие Replay для события вебхука возвращает его статус к
received и пишет строку аудита,
но строки в статусе received никто не потребляет — воркера повторной обработки нет. Пропущенный
расчёт восстанавливается сверкой, а не повтором.Сверка: когда вебхук так и не пришёл
Вебхук может потеряться, задержаться или быть заблокированным. Сверка спрашивает у провайдера, что произошло на самом деле, и применяет ответ через тот же идемпотентный расчёт, поэтому она может работать рядом с вебхуком, не зачисляя деньги дважды.- Автоматически. Проход
payment_reconcileна воркере раз в минуту опрашивает до 100 намерений платежа, которые находятся вpendingилиprocessing, относятся кcryptobotилиyookassa, имеют ссылку провайдера и не обновлялись минуту. - Клиентом.
POST /v1/account/orders/{orderID}/refresh, а также экран заказа в Telegram при открытии. Сверка срабатывает, только пока заказ ещё ждёт оплаты, и молча ничего не делает для адаптера, который нельзя опросить. - Оператором.
POST /v1/admin/payments/{paymentID}/reconcileс операторским токеном.
/admin/finance → Stuck перечисляет намерения платежа, которые находятся в пути дольше, чем
должен занимать любой поддерживаемый провайдер.
Возвраты по провайдерам
Возврат никогда не переписывает историю платежей: это новая строка вrefunds плюс новая
сбалансированная проводка в книге, поэтому запись о том, что было списано, сохраняется.
succeeded, валюта не совпадает, сумма не положительна или превышает
намерение, адаптер не объявляет возвраты, у намерения нет ссылки провайдера или уже зарезервированные
возвраты вместе с этой суммой превысили бы то, что провайдер получил. Изменения возвратов
сериализуются для каждого намерения рекомендательной блокировкой, а переиспользование ключа с другими
параметрами отклоняется.
Кошельковую часть заказа никогда нельзя вернуть провайдеру: сумма ограничена тем, что провайдер
действительно получил. Полный возврат переводит заказ в
refunded, частичный — в
partially_refunded, и оба ставят в очередь уведомление оператору.
Как убедиться, что провайдер действительно работает
Надёжной проверки подключения внутри панели нет.POST /v1/panel/settings/providers/{provider}/test
действительно выполняет настоящую проверку только на чтение — getMe для CryptoBot, один платёж из
списка для YooKassa, getMe для Stars, ничего для manual — со своим дедлайном в 20 секунд, и эта
проверка не может создать платёж. Но результат, который она пытается сохранить (passed, failed,
unsupported), не входит в набор значений, принимаемых колонкой (unknown, healthy, failing),
поэтому запись падает и запрос отвечает 500 operation_failed независимо от того, сработали
учётные данные или нет. Результат проверки остаётся в логе сервера API; колонка вебхуков в панели
точно так же всегда показывает unknown, потому что здоровье вебхуков никто не фиксирует.
Что делать вместо этого:
- Выставьте версию тарифа на продажу по минимально возможной цене и купите её сами — в тестовой сети, если она у провайдера есть.
- Посмотрите в
/admin→ System → Webhooks строку приёма и её статус. - Посмотрите на
/admin/finance/{orderId}ленту событий намерения платежа —created,status_changedи любая классификация несоответствия видны там. - Верните деньги через поверхность с операторским токеном — это проверит вторую половину интеграции.
Работа с деньгами, общая для всех провайдеров
- Суммы везде выражены целыми числами в минорных единицах, а показатель степени берётся из
валюты:
XTR,JPYиKRW— без дробной части, у большинства остальных их две. - Каждое изменение у провайдера несёт ключ идемпотентности Omniflow. Клиентский ключ —
order-<orderID>-<provider>: он привязан к обоим, потому что ключ одного лишь заказа связал бы вторую попытку с другим способом оплаты с намерением первого адаптера. Продления используютrenewal:<cycleKey>:<attempt>. На/v1/adminключ ваш, от 8 до 128 символов. - Платёж, не соответствующий своему заказу, всё равно фиксирует событие. Несовпадение валюты, дубль, недоплата, переплата и позднее поступление имеют собственные классификации, и ни одна из них не может выдать право на услугу. Пополнение — исключение: оно ничего не должно, кроме самих денег, поэтому пере- или недоплата зачисляет ровно то, что пришло.
wallet_unavailableстоит уметь распознавать: провайдер получил деньги, но кошельковой части, на которую рассчитывал заказ, больше нет, поэтому заказ остаётся в ожидании. Автоматически это не разрешается — ситуация появляется в ленте событий заказа и требует оператора.- HTTP-вызовы к провайдерам ограничены: клиентский таймаут 15 секунд и лимит ответа 1 МиБ.