Розничная платформа
Розничная платформа Mockarty позволяет покупателям оформить лицензию
на вашем сайте. После оплаты покупатель получает ключ активации по почте.
Когда платёжный провайдер подтверждает возврат или отмену платежа,
соответствующая лицензия отзывается.
Этот раздел адресован администраторам лицензионного сервера.
Покупательский путь со стороны клиента описан в
разделе о доступе к функциям.
Поддерживаемые сервисы
Для оплаты через страницу провайдера можно подключить YooKassa, CloudPayments,
Т-Банк, Robokassa или Stripe. Настройте нужного провайдера на странице
«Платёжные провайдеры». Фискальный чек может оформить эквайер или АТОЛ.
Розничная платформа также поддерживает возвраты, отзыв лицензий, отправку писем
с повторами и запуск нескольких экземпляров с общими PostgreSQL и Redis.
Розничная подсистема по умолчанию выключена. Настройте провайдеров в
админке (страница «Платёжные провайдеры») или через переменные окружения
ниже — в обоих случаях покупательские endpoints включаются только для
настроенных провайдеров. Если ни один не настроен, эти endpoints возвращают
503: ничего не публикуется без явной настройки.
Шаг 1 — выбрать платёжного провайдера
Два способа настройки, свободно комбинируются:
- Админка (рекомендуется) — откройте страницу «Платёжные провайдеры».
У каждого эквайера (YooKassa, CloudPayments, Т-Банк, Robokassa, Stripe) и у
фискальной кассы (АТОЛ) — карточка с полями учётных данных; заполните,
Проверить, Включить, при необходимости Сделать основным. Креды
хранятся в БД, шифруются на диске при заданномRETAIL_CONFIG_ENC_KEY.
Изменения применяются сразу и расходятся по всем нодам кластера в пределах
RETAIL_PROVIDER_RELOAD_SECONDS(по умолчанию 60). - Переменные окружения — YooKassa и Stripe можно задать переменными ниже
(используются как fallback только когда в админке не настроен ни один
провайдер). Остальные эквайеры и фискальный провайдер настраиваются только в
админке.
Ответ /api/public/v1/retail/plans сообщает, какие провайдеры активны, и
лендинг отрисовывает соответствующие кнопки.
YooKassa (российский рынок)
Переменные окружения для лицензионного сервера:
RETAIL_YOOKASSA_SHOP_ID=123456
RETAIL_YOOKASSA_SECRET_KEY=live_********
# Опционально: общий HMAC-секрет, который ваш обратный прокси
# добавляет в заголовок X-Mockarty-Signature — дополнительная
# защита вебхуков.
RETAIL_YOOKASSA_WEBHOOK_HMAC=any-long-random-string
Вебхуки YooKassa не несут собственной подписи тела. Лицензионный
сервер защищается от подделок, перепрашивая каждый платёж по
/v3/payments/{id} и отклоняя вебхук, если сумма или статус в
теле не совпадают с ответом сервера. Опциональный
X-Mockarty-Signature добавляет второй слой защиты.
Stripe (международный рынок)
RETAIL_STRIPE_SECRET_KEY=sk_live_********
RETAIL_STRIPE_WEBHOOK_SECRET=whsec_********
Webhook-секрет — это значение из раздела
Developers → Webhooks → ваш endpoint → Signing secret.
Лицензионный сервер проверяет заголовок Stripe-Signature через
HMAC-SHA256 с допуском 5 минут на расхождение часов; так делает и
официальный SDK от Stripe.
Другие эквайеры (настраиваются в админке)
CloudPayments, Т-Банк (Tinkoff) Касса и Robokassa подключаются на
странице «Платёжные провайдеры» (без переменных окружения). Все — российские
эквайеры с картами + СБП; Т-Банк и Robokassa поддерживают самозанятых (НПД).
Заполните поля на карточке (CloudPayments: Public ID + API-секрет; Т-Банк: ключ
терминала + пароль; Robokassa: логин магазина + Пароль №1/№2), нажмите
Проверить, затем Включить.
Возвраты Robokassa делаются в кабинете мерчанта и не отзывают лицензию
автоматически — отзовите её вручную из админки.
Фискальные чеки 54-ФЗ
Выберите фискального провайдера на странице «Платёжные провайдеры» (раздел
«Фискализация»):
- Inline — эквайер выпускает чек сам (Mockarty передаёт чек в запросе
оплаты). Доступно с YooKassa. - АТОЛ Онлайн — облачная касса / ОФД, выпускает чек на стороне сервера;
работает с любым эквайером. Заполните login, password, код группы, ИНН +
email компании. - Нет — чек не выпускается (например, B2B-перевод, освобождённый от 54-ФЗ).
Ставка НДС в чеке берётся из профиля продавца. Результаты фискализации
сохраняются по каждому платежу и идемпотентны при повторной доставке вебхука.
Адреса для регистрации в дашборде провайдера
| Провайдер | URL |
|---|---|
| YooKassa | https://<ваш-license-server>/api/public/v1/retail/checkout/yookassa |
| CloudPayments | https://<ваш-license-server>/api/public/v1/retail/checkout/cloudpayments |
| Т-Банк | https://<ваш-license-server>/api/public/v1/retail/checkout/tbank |
| Robokassa | https://<ваш-license-server>/api/public/v1/retail/checkout/robokassa |
| Stripe | https://<ваш-license-server>/api/public/v1/retail/checkout/stripe |
Оба URL анонимны намеренно — авторизация происходит по подписи
запроса от провайдера.
Шаг 2 — настроить исходящую почту
Лицензионный сервер кладёт письма в outbox-таблицу при покупке,
возврате и повторной отправке. Чтобы аутбокс реально слал
письма, заполните SMTP-переменные:
RETAIL_SMTP_HOST=smtp.example.com
RETAIL_SMTP_PORT=587 # 465 — implicit TLS
RETAIL_SMTP_USERNAME=mockarty@example.com
RETAIL_SMTP_PASSWORD=********
RETAIL_SMTP_FROM=licences@example.com
RETAIL_BASE_URL=https://app.example.com # используется в ссылках в письмах
Если RETAIL_SMTP_HOST не задан, аутбокс простаивает —
письма копятся в БД, пока вы либо подключите SMTP, либо вручную
отправите их через админку. Лицензия при этом всё равно выдаётся,
покупатель может забрать ключ через админ-панель; авто-отправка
просто откладывается.
Аутбокс работает под продвинутым advisory-локом Postgres, так что
мульти-репликовый деплой не приведёт к двойной отправке. Не
доставленные письма экспоненциально откладываются (1 мин → 2 →
4 → … до 1 ч), всего до 10 попыток; затем строка попадает в
email_deliveries с заполненным failed_at / last_error для
триажа эксплуатации.
Шаг 3 — проверить каталог
При первом запуске появляется тарифный план retail. Откройте
Тарифные планы → retail → компоненты, проверьте и настройте цены своей
установки до публикации предложений для покупателей. Ориентируйтесь на
значения в админ-панели: они могут отличаться между установками и меняться
со временем.
Сейчас розничная котировка доступна только в рублях. Не передавайте поле
currency или укажите "RUB". Для другой валюты API вернёт market_unavailable.
Шаг 4 — подключить лендинг
Лендинг ходит в три endpoint’а:
GET /api/public/v1/retail/plans— отдаёт каталог и
список активных провайдеров. Лендинг отрисовывает по кнопке
на каждый.POST /api/public/v1/retail/quote— отправляет корзину,
получаетpayment_urlиquote_id. Покупателя перенаправляют
наpayment_url.GET /api/public/v1/retail/licenses/validate?key=…— публичная
проверка отзыва для ваших собственных интеграций: отвечает, активна
ли лицензия, истекла или возвращена. Десктоп и CLI Mockarty проверяют
лицензию локально и этот endpoint не вызывают, так что изолированной
установке от него ничего не нужно. Всегда отвечает 200 — лицензионный
сервер не выдаёт информацию о существовании лицензии вне
аутентифицированных потоков.
Пример запроса quote:
POST /api/public/v1/retail/quote
{
"customer_email": "buyer@example.com",
"customer_name": "Иван Покупатель",
"feature_seats": { "mock": 3, "api-tester": 2, "security": 1 },
"duration_days": 365,
"provider": "yookassa",
"currency": "RUB",
"return_url": "https://app.example.com/checkout/done"
}
В новых интеграциях используйте security. Старые клиенты, созданные до
переименования модуля безопасности, могут продолжать отправлять ключ fuzz или
security_agent: сервер преобразует его в security до расчёта и фиксации
quote. Не отправляйте больше одного из этих равнозначных ключей в одной корзине
— неоднозначный запрос будет отклонён, а не посчитан дважды.
Ответ:
{
"quote_id": "q_3f2c…",
"total_amt": 560000,
"currency": "RUB",
"expires_at": "2026-05-08T12:30:00Z",
"payment_url": "https://yookassa.ru/checkouts/3f2c…",
"provider": "yookassa",
"payment_reference": "py_3f2c…"
}
HTTP 200 означает, что license-server сохранил точный идентификатор платежа у
провайдера, а не просто отправил провайдеру запрос. Если endpoint вернул
provider_error или db_error, не отправляйте новую корзину автоматически:
сначала проверьте существующий платёж в кабинете провайдера и передайте
неоднозначную попытку оператору.
Quote живёт 30 минут. После этого покупатель должен оформить
заново с актуальной ценой — это позволяет менять промо без
зависания корзин на старых тарифах.
В начальном профиле розничного рынка активен прайс-лист в RUB.
Quote отклоняет другую валюту до обращения к платёжному
провайдеру. Сначала добавьте проверенный прайс-лист для нового
рынка и только затем показывайте такую валюту на лендинге.
Шаг 5 — возвраты
Два сценария:
-
Со стороны провайдера — клиент оспорил списание у банка
или менеджер магазина оформил возврат в кабинете провайдера.
Прилетает webhookrefund.succeeded, лицензия автоматически
отзывается, на e-mail клиента уходит письмо
retail_refunded. -
Со стороны администратора — сессия владельца или администратора
делает POST/api/v1/retail/licenses/<id>/refundс опциональным
телом{"reason": "...", "amount_minor": ...}. Лицензионный
сервер считает пропорциональную неиспользованную часть
(еслиamount_minorне задан) и просит провайдера сделать
возврат. Лицензия отзывается после полного terminal-ответа провайдера или
проверенного webhookrefund.succeeded, если идентификаторы и сумма
совпадают с исходным возвратом.
Для автоматизации в партнёрском API также остаются
/api/public/v1/retail/licenses/<id>/refund и соответствующий endpoint
.../<id>/email для повторной отправки. Партнёрский API-ключ может
использовать их только для лицензии, связанной с оплаченным заказом
этого же партнёра. Прямая retail-лицензия или лицензия другого партнёра
возвращает 403 без обращения к платёжному или почтовому провайдеру.
Синхронный ответ провайдера отзывает лицензию только при точном совпадении
идентификатора возврата, платёжной ссылки, валюты и суммы с запросом.
Неполный или несовпадающий ответ возвращает 502, оставляет лицензию
активной и сохраняет операцию для ручной проверки.
Формула пропорционального возврата:
возврат = заплачено × (всего_дней − прошло_дней) ÷ всего_дней
Лицензия, отозванная на следующий день после оплаты, возвращает
~99 % суммы; отозванная за неделю до истечения — ~2 %.
Эксплуатация — что мониторить
Задайте LICENSE_SERVER_METRICS_TOKEN отдельным случайным bearer-токеном, чтобы
включить GET /metrics. Если переменная пуста, маршрут не регистрируется.
Оставьте его в приватной сети мониторинга и передавайте
Authorization: Bearer <token> при scrape.
| Метрика | На что смотреть |
|---|---|
mockarty_license_retail_provider_dispatches |
Текущие попытки оплаты/возврата по провайдеру и сохранённому статусу. |
mockarty_license_retail_provider_dispatch_attempts |
Рост попыток, который может означать нестабильность провайдера или сети. |
mockarty_license_retail_provider_operator_required |
Денежные операции, которые автоматика отказалась повторять. Alert при любом значении выше нуля. |
mockarty_license_retail_provider_oldest_due_age_seconds |
Возраст самой старой попытки, ожидающей восстановления. Alert при превышении обычного интервала recovery. |
В labels этих метрик намеренно остаются только ограниченные значения
provider, operation и status. Идентификаторы лицензий, quote, платежей,
возвратов, dispatch и idempotency в метрики не попадают.
При неоднозначном результате вызова license-server повторяет операцию только
для провайдера с гарантией точного идемпотентного replay. Иначе попытка
останавливается для оператора. Сверьте исходную оплату/возврат в кабинете
провайдера и не создавайте новую операцию, пока результат не установлен.
Сигналы о злоупотреблении лицензией
Лицензионный сервер записывает — и никогда не применяет сам — три сигнала по
лицензии: активацию запросила машина сверх слотов устройств лицензии, за сутки
появилось необычно много новых устройств, или отчитывается больше экземпляров,
чем разрешено лицензией. Каждый сигнал появляется раз в сутки на лицензию в
разделе KMS → Нарушения и остаётся там, пока оператор его не закроет.
Задайте LICENSE_ABUSE_ALERT_EMAIL — почтовый ящик операторов, — чтобы каждый
новый сигнал приходил ещё и письмом (через исходящую почту из Шага 2). Письмо
называет лицензию и сигнал; ничего не блокируется автоматически — блокировка
лицензии остаётся ручным действием оператора.
Решение проблем
| Симптом | Что делать |
|---|---|
503 retail_not_configured на /retail/quote |
Не задан ни один провайдер. Добавьте credentials YooKassa или Stripe и перезапустите license-server. |
503 pricing_unavailable на /retail/quote |
Активный прайс-лист отсутствует или рассинхронизирован. Не повторяйте оплату; попросите оператора восстановить цены. |
400 market_unavailable на /retail/quote |
Запрошенная валюта или продукт недоступны в активном розничном рынке. |
400 zero_price на /retail/quote |
Скидки уменьшили сумму к оплате до нуля. Используйте поддерживаемую конфигурацию платного checkout. |
502 provider_error или 500 db_error на /retail/quote |
Не создавайте новый платёж автоматически. Проверьте кабинет провайдера и метрику operator_required на неоднозначную попытку. |
503 dispatch_unavailable на /retail/quote |
Не удалось получить право на сохранённую попытку оплаты. Повторяйте только после подтверждения оператора, что активного платежа нет. |
401 invalid_signature на /retail/checkout/... |
Подпись провайдера не сошлась — секрет был пересоздан. Сохраните webhook заново в дашборде провайдера. |
| Письмо в очереди, но не доставлено | Проверьте показанную оператору ошибку доставки. Чаще всего проблема в SMTP-авторизации или DNS. |
| Покупатель оплатил, но не получил ключ | Проверьте статусы оплаты и доставки, затем повторно отправьте ключ через аутентифицированный admin flow. |
| Лицензия продолжает работать после возврата | Webhook refund.succeeded ещё не пришёл. Подождите несколько минут или проверьте статус в кабинете провайдера. |
За подробностями по архитектуре обращайтесь к поставщику Mockarty.