Skip to main content
Omniflow проводит деньги через единый контракт, основанный на объявляемых возможностях. Каждый адаптер реализует одни и те же шесть методов — имя, возможности, создание, опрос, возврат, проверка вебхука — и три необязательных уточнения: списание с сохранённого способа оплаты, восстановление счёта по ссылке мерчанта и объявление валют, в которых он проводит расчёты. Всё, что находится ниже этого контракта (сверка, возвраты, чеки, операторская лента событий), работает с собственной записью Omniflow payment_intents, а не с объектной моделью конкретного провайдера. Объявление возможностей вместо особой обработки каждого провайдера — это то, что позволяет экрану объяснить, почему что-то не предлагается, а не просто отказать. Тариф с ценой только в RUB не покажет Telegram Stars, а у платежа через CryptoBot не будет кнопки возврата, потому что оба факта приходят от самого адаптера. Эта страница — руководство по настройке. Конечный автомат заказа, классификации расчётов и то, что порождает оплаченный заказ, описаны в заказах и платежах.

Четыре адаптера

Как адаптер становится доступным

Адаптеры собираются из переменных окружения при старте процесса, и в API, и в воркере, поэтому у двух процессов не может оказаться разного набора провайдеров — иначе получился бы отказ, при котором воркер не может списать продление через того провайдера, которым платил клиент.
  • manual и telegram_stars регистрируются всегда.
  • cryptobot регистрируется, когда задан APP_CRYPTOBOT_TOKEN.
  • yookassa регистрируется, когда задан APP_YOOKASSA_SHOP_ID или APP_YOOKASSA_SECRET, и затем отказывается собираться, если присутствуют не оба значения.
Пустые учётные данные оставляют адаптер незарегистрированным, а не регистрируют сломанный, — поэтому у установки, которая не продаёт через YooKassa, нет адаптера YooKassa, падающего при первом использовании. Запрос ненастроенного провайдера отвечает 422 provider_unavailable.

Какие способы предлагаются клиенту

Способ оплаты появляется, только если установка его настроила и тариф имеет цену в валюте, в которой этот способ проводит расчёты. Выбор никогда не откатывается к несовместимому адаптеру, а заказу, который уже полностью покрыт кошельком, не предлагается ни один провайдер. У пополнений кошелька нет прайс-листа тарифа, поэтому они предлагают все включённые адаптеры, поддерживающие валюту. Список, который видит клиент, отсортирован по слагу провайдера — cryptobot, manual, telegram_stars, yookassa, — и обе поверхности используют одну и ту же сортировку, поэтому способ оплаты никогда не окажется на одной позиции в боте и на другой в браузере. Поле порядка отображения в панели этого не меняет.
Адаптер manual регистрируется всегда, и переключателя, который бы его скрыл, нет. Он также не объявляет никаких ограничений по валюте, поэтому подходит к любой цене тарифа. Установка, которая никогда не собиралась принимать банковские переводы, всё равно покажет manual как способ оплаты каждому клиенту и в боте, и в веб-панели, а каждое созданное им намерение платежа будет ждать решения оператора, которое можно принять только на поверхности с токеном. См. Ручные платежи.
На /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 здесь не рассматривается.
Два ограничения Stars, которые нужно учитывать. Poll не поддерживается, поэтому платёж через Stars нельзя сверить, и кнопка Обновить у клиента для него ничего не делает — поток обновлений остаётся единственным путём расчёта. И платёж через Stars, начатый из веб-панели, не записывает идентификатор пользователя Telegram, поэтому такое списание вообще нельзя вернуть через адаптер. Покупки за Stars, которые могут потребовать возврата, следует делать в боте.

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.
CryptoBot не поддерживает возвраты. Адаптер возвращает «unsupported», а платёжный сервис отказывает ещё до обращения к провайдеру. Если вы продаёте через CryptoBot, ваш путь возврата — зачисление в кошелёк: операторская корректировка кошелька клиента с причиной и ключом идемпотентности, которая записывается в книгу проводок и видна клиенту. См. кошелёк и книгу проводок.

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 никогда не выставляет save_payment_method, поэтому идентификатор способа оплаты никогда не возвращается и его не к чему привязать, а список сохранённых способов остаётся пустым. Рекуррентные списания объявлены и подключены, но сейчас их невозможно активировать. Рекуррентные платежи описывают ровно то, какие части работают.

Ручные платежи

Адаптер без внешней системы. Create возвращает намерение платежа, у которого ссылка провайдера — это идентификатор заказа, а статус — requires_action; опрашивать нечего и вебхук проверять нечем. Расчёт происходит, когда его подтверждает оператор, — именно это делает адаптер пригодным для банковских переводов, наличных и любых каналов, которые Omniflow не интегрирует. Поскольку деньги перемещались вне системы, ответственность несёт само подтверждение: оно отклоняется без указанного оператора, причины и корректного ключа идемпотентности.
Решение записывается в manual_payment_approvals, уникально для каждого намерения платежа, поэтому второе решение, противоречащее первому, отклоняется с сообщением «manual payment already has a different final decision». Подтверждение проводит полную сумму намерения через тот же путь расчёта, что используют все остальные провайдеры. Отклонение фиксирует решение и не проводит ничего. Клиент видит передачу платежа в режим manual: панель сообщает, что подтверждать будет оператор, а экран заказа в Telegram говорит то же самое, пока заказ в ожидании.
Операторского экрана для ручных платежей нет. POST /v1/admin/payments/{paymentID}/manual-decision существует только на поверхности с bearer-токеном — нет ни маршрута /v1/panel, ни кнопки в /admin, ни чего-либо в операторских инструментах Telegram. Если вы принимаете ручные платежи, вам понадобится скрипт или HTTP-клиент и операторский токен. См. интеграционный API.

Приём вебхуков

Один публичный маршрут обслуживает каждый адаптер, объявляющий поддержку вебхуков: POST /v1/payments/webhooks/{provider}. Он не несёт учётных данных — подлинность обеспечивается проверкой подписи адаптера, а не токеном в URL, — а тело ограничено 64 КиБ (413 payload_too_large сверх этого). Что происходит с запросом, по порядку:
  1. Отказ, если названный адаптер не зарегистрирован или не объявляет поддержку вебхуков. Именно это закрывает маршрут для telegram_stars и manual.
  2. Вычисляется дайджест тела и запускается проверка адаптера. Неудача всё равно записывается — строкой с signature_valid = false и идентификатором события invalid:<digest> — и отвечает 400 webhook_rejected. Отклонённый вебхук, не оставивший следа, был бы неотличим от того, который вообще не приходил.
  3. Вставляется строка события. Если сохранённая строка уже обработана или проигнорирована, ответ — {"status": "duplicate"}, и на этом всё. Это защита от дублей, привязанная к собственному идентификатору события провайдера, а не к какому-либо заголовку Omniflow.
  4. Только для YooKassa — платёж заново запрашивается через аутентифицированный API.
  5. Проверенное, но неуспешное событие завершается как ignored, а не failed. Истёкший счёт — не ошибка.
  6. Находится намерение платежа, расчёт проводится по общему идемпотентному пути, и возвращается 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 перечисляет намерения платежа, которые находятся в пути дольше, чем должен занимать любой поддерживаемый провайдер.
Кнопка Reconcile на /admin/finance пишет событие аудита (panel.payment.reconciled) и больше ничего — панель не держит платёжный сервис и никогда не опрашивает провайдера заново. То же относится к форме Refund в панели, которая записывает panel.refund.issued, не создавая возврата, не двигая книгу проводок и не меняя состояние заказа. Работающие пути — POST /v1/admin/payments/{paymentID}/reconcile и POST /v1/admin/payments/{paymentID}/refunds на поверхности с операторским токеном.

Возвраты по провайдерам

Возврат никогда не переписывает историю платежей: это новая строка в 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, потому что здоровье вебхуков никто не фиксирует. Что делать вместо этого:
  1. Выставьте версию тарифа на продажу по минимально возможной цене и купите её сами — в тестовой сети, если она у провайдера есть.
  2. Посмотрите в /admin → System → Webhooks строку приёма и её статус.
  3. Посмотрите на /admin/finance/{orderId} ленту событий намерения платежа — created, status_changed и любая классификация несоответствия видны там.
  4. Верните деньги через поверхность с операторским токеном — это проверит вторую половину интеграции.

Работа с деньгами, общая для всех провайдеров

  • Суммы везде выражены целыми числами в минорных единицах, а показатель степени берётся из валюты: XTR, JPY и KRW — без дробной части, у большинства остальных их две.
  • Каждое изменение у провайдера несёт ключ идемпотентности Omniflow. Клиентский ключ — order-<orderID>-<provider>: он привязан к обоим, потому что ключ одного лишь заказа связал бы вторую попытку с другим способом оплаты с намерением первого адаптера. Продления используют renewal:<cycleKey>:<attempt>. На /v1/admin ключ ваш, от 8 до 128 символов.
  • Платёж, не соответствующий своему заказу, всё равно фиксирует событие. Несовпадение валюты, дубль, недоплата, переплата и позднее поступление имеют собственные классификации, и ни одна из них не может выдать право на услугу. Пополнение — исключение: оно ничего не должно, кроме самих денег, поэтому пере- или недоплата зачисляет ровно то, что пришло.
  • wallet_unavailable стоит уметь распознавать: провайдер получил деньги, но кошельковой части, на которую рассчитывал заказ, больше нет, поэтому заказ остаётся в ожидании. Автоматически это не разрешается — ситуация появляется в ленте событий заказа и требует оператора.
  • HTTP-вызовы к провайдерам ограничены: клиентский таймаут 15 секунд и лимит ответа 1 МиБ.
Справочник по конфигурации для всех этих переменных — в конфигурации; как платёж выглядит со стороны клиента, описано в веб-панели и в Telegram-боте.