Lemon Squeezy — международные платежи картой¶
Lemon Squeezy (LS) — платёжная платформа с моделью Merchant of Record: они выступают продавцом от своего имени, сами собирают налоги (VAT/GST) по всему миру и принимают Visa/Mastercard от иностранных пользователей.
Почему не Тинькофф для иностранцев? С марта 2022 года Visa и Mastercard
прекратили обработку транзакций у российских эквайеров — иностранные карты
получают DECLINED независимо от валюты счёта. Tinkoff физически не может
принять оплату с зарубежной карты.
Архитектура интеграции¶
Пользователь нажимает "Pay with card" в дашборде
→ POST /api/billing/init-ls {tariffId}
→ BillingController строит checkout URL напрямую (без API call к LS):
https://<storeSlug>.lemonsqueezy.com/checkout/buy/<variantId>
?checkout[email]=...&checkout[custom][user_id]=...
→ Браузер открывает URL и переходит на страницу оплаты LS
→ Оплата проходит
→ LS шлёт webhook на POST /api/billing/notify-ls
X-Signature: HMAC-SHA256(rawBody, signing_secret)
event: subscription_created | subscription_payment_success
→ BillingController добавляет 30 дней в user_tariffs
→ Пользователь возвращается на /auth/protected?ls_success=1
Почему URL, а не API? Cloudflare блокирует server-side запросы с datacenter/VPS IP к
api.lemonsqueezy.com. URL-подход (браузер открывает checkout напрямую) обходит это ограничение без изменений в логике приёма платежей.
Типы событий подписки:
| Событие | Когда | Действие бэкенда |
|---|---|---|
subscription_created |
Первый платёж при оформлении | +30 дней в пул |
subscription_payment_success |
Ежемесячное автосписание | +30 дней в пул |
subscription_cancelled |
Пользователь отменил подписку | Только лог; дни истекают сами |
Идемпотентность: payments.ls_order_id = уникальный ID события (ID подписки
или ID инвойса) — дублирующий webhook просто вернёт 200 already processed.
Часть 1 — Настройка аккаунта в Lemon Squeezy¶
1.1 Регистрация¶
- Зайди на app.lemonsqueezy.com
- Регистрируйся — поддерживаются физлица из РФ и других стран
- После входа попадёшь в Dashboard
1.2 Заполнить профиль продавца (обязательно для вывода денег)¶
Settings → Your profile:
- Имя, адрес, ИНН/Tax ID (для физлиц из РФ — ИНН)
- Способ вывода: банковская карта или счёт (Wise, PayPal и т.д.)
1.3 Подключить платёжный метод для биллинга платформы¶
LS берёт комиссию с продаж (~5% + $0.50). Нужна карта в Settings → Billing.
Часть 2 — Создание магазина¶
2.1 Создать магазин¶
Settings → Stores → Add store:
- Name: dix.su (или любое)
- Slug: dixsu (используется в checkout URL)
- Currency: USD (рекомендуется для международной аудитории)
После создания в строке магазина будет Store ID (#12345) — запиши его.
Часть 3 — Создание продуктов (Subscription)¶
Нужно три продукта — по одному на каждый тариф. Для каждого:
Products → Add product → Subscription
Настройки продукта¶
| Тариф | Name | Price | Billing period |
|---|---|---|---|
| VIP | VIP — 30 days |
$4.99 |
Monthly |
| PRO | PRO — 30 days |
$7.99 |
Monthly |
| PERS | PERS — 30 days |
$12.99 |
Monthly |
Trial period — оставить пустым (платный с первого дня).
Checkout passthrough / Custom data — НЕ заполнять здесь; user_id и
tariff_id передаются программно при создании checkout через API.
Получить Variant ID¶
После сохранения продукта: Products → [название] → Variants.
У каждого варианта справа будет его ID (число, например 823456).
Записать ID для VIP, PRO и PERS.
Часть 4 — API Key¶
Settings → API → Add API key:
- Название: dixsu-prod
- Нажать Create
- Скопировать ключ немедленно — показывается только один раз
Часть 5 — Webhook¶
Settings → Webhooks → Add webhook:
Callback URL:
Signing secret — придумываешь сам (произвольная строка ≥ 32 символа). Сгенерировать случайную:
Events — отметить три:
- ✅ subscription_created
- ✅ subscription_payment_success
- ✅ subscription_cancelled
Сохранить. Signing secret больше нигде не показывается — запиши его.
Часть 6 — Переменные окружения на VPS¶
Добавить в конец файла:
# === Lemon Squeezy ===
LS_STORE_SLUG=dixsu # Slug магазина из шага 2.1 (Settings → Stores → Slug)
LS_API_KEY=eyJ0eXAiOiJKV1QiLCJhbGci... # API Key из шага 4
LS_STORE_ID=12345 # Store ID из шага 2.1
LS_SIGN_SECRET=a1b2c3d4e5f6... # Signing secret из шага 5
LS_VARIANT_VIP_30D=823456 # Variant ID VIP из шага 3
LS_VARIANT_PRO_30D=823457 # Variant ID PRO
LS_VARIANT_PERS_30D=823458 # Variant ID PERS
Сохранить (Ctrl+O, Ctrl+X), затем перезапустить:
Часть 7 — Проверка¶
7.1 Убедиться что кнопка появилась¶
Зайти на https://dix.su/auth/protected → раздел Тарифы. У каждого
платного тарифа должна появиться кнопка "💳 Pay with card $X.XX" рядом
с кнопкой Тинькофф (или вместо неё, если Тинькофф не настроен).
7.2 Протестировать webhook без реальной оплаты¶
В LS Dashboard:
Выбрать subscription_created → нажать Send. Проверить логи modulauth:
Ожидаемое в логах:
[ls] subscription_created userId=null tariff=null eventId=test-...
[ls] webhook missing required fields: ...
Тестовый ивент не содержит custom_data — это нормально. Главное что
вебхук дошёл и подпись прошла (если был бы invalid signature — значит
LS_SIGN_SECRET неверный).
7.3 Тестовая оплата¶
LS имеет тестовый режим (Test mode в шапке Dashboard).
В тестовом режиме checkout принимает тестовые карты:
| Карта | Результат |
|---|---|
4242 4242 4242 4242 |
Успешная оплата |
4000 0000 0000 0002 |
Отказ |
Дата — любая в будущем, CVV — любые 3 цифры.
Провести тестовую оплату → проверить что в истории платежей появилась
запись, а в user_tariffs добавились 30 дней:
docker exec dixsu-postgres-1 psql -U dixsu -c \
"SELECT role, remaining_days, status FROM user_tariffs WHERE user_id = <ID> ORDER BY id DESC LIMIT 5;"
Часть 8 — Производственный режим¶
После успешного теста переключить магазин в production:
Settings → Stores → [магазин] → переключатель Test mode → OFF
Variant IDs в production могут отличаться от тестовых — пересмотреть и
обновить LS_VARIANT_* в .env если нужно.
Диагностика¶
Webhook не доходит¶
# Проверить доступность endpoint извне
curl -s -o /dev/null -w "%{http_code}" -X POST https://dix.su/api/billing/notify-ls \
-H "Content-Type: application/json" -d '{}'
# Должен вернуть 401 (подпись не совпадает) — значит endpoint доступен
Если 404 — modulauth не задеплоен или SecurityConfig не включил путь.
Подпись не совпадает (401 в логах)¶
Причина: LS_SIGN_SECRET в .env не совпадает с Signing Secret в LS Dashboard.
Проверить пробелы и переводы строк при копировании.
Дни не начислились¶
Смотреть строки вида [ls] subscription_created userId=... tariff=....
Если userId=null — custom_data не пришёл от LS. Убедиться что checkout
создаётся через /api/billing/init-ls, а не через прямую ссылку на продукт.
Дублирующий webhook (начислено дважды)¶
Не должно произойти — защищено по ls_order_id. Проверить:
docker exec dixsu-postgres-1 psql -U dixsu -c \
"SELECT ls_order_id, COUNT(*) FROM payments WHERE provider='lemonsqueezy'
GROUP BY ls_order_id HAVING COUNT(*) > 1;"
Изменение цен¶
Цены управляются в LS Dashboard (Products → Variants → Edit price).
В коде цены для отображения на кнопке хранятся в LemonSqueezyService.USD_CENTS —
обновить при изменении, пересобрать modulauth:
// LemonSqueezyService.java
public static final Map<String, Integer> USD_CENTS = Map.of(
"vip_30d", 499, // ← изменить
"pro_30d", 799,
"pers_30d", 1299
);
Чеки НПД для платежей через LS¶
Платежи через Lemon Squeezy не создают НПД-чеки — LS сам выступает продавцом и выдаёт свой инвойс клиенту. Это корректно: дважды выставлять чек за одну продажу нельзя. Если бизнес-форма сменится на ИП с онлайн-кассой, LS-платежи также не требуют фискализации через российскую кассу — продажа юридически совершена от имени LS (ирландская юрисдикция).
Справочник env-переменных¶
| Переменная | Откуда взять | Пример |
|---|---|---|
LS_STORE_SLUG |
Settings → Stores → поле Slug под названием | dixsu |
LS_API_KEY |
Settings → API → ключ (показывается один раз) | eyJ0eXAiOiJKV1... |
LS_STORE_ID |
Settings → Stores → #XXXXX |
12345 |
LS_SIGN_SECRET |
Settings → Webhooks → Signing Secret | a1b2c3d4... |
LS_VARIANT_VIP_30D |
Products → VIP → Variants → ID | 823456 |
LS_VARIANT_PRO_30D |
Products → PRO → Variants → ID | 823457 |
LS_VARIANT_PERS_30D |
Products → PERS → Variants → ID | 823458 |
Где смотреть код¶
| Файл | Что делает |
|---|---|
modulauth/.../billing/LemonSqueezyService.java |
Создание checkout, верификация подписи webhook |
modulauth/.../billing/BillingController.java |
POST /api/billing/init-ls, POST /api/billing/notify-ls |
modulauth/.../config/SecurityConfig.java |
/api/billing/notify-ls разрешён без авторизации |
modulauth/.../db/migration/V45__Add_ls_payments.sql |
provider, currency, ls_order_id в таблице payments |
templates/protected-page.html |
Кнопка "💳 Pay with card", баннер ?ls_success=1 |