Короткий ответ — и почему он не так прост
Если ваши пользователи в России — берите ЮKassa. Если международный рынок — Stripe. Статью можно закрывать.
Но вот ситуация из реальной жизни. Прошлый год: e-commerce платформа, аудитория — СНГ плюс Европа. Основная выручка в рублях, но 20% заказов идут из Казахстана, Германии, Кипра. Один шлюз не покрывает всех. Два шлюза — два набора API, два потока webhook, два формата ошибок. И вот тут начинается настоящая инженерная работа.
Эта статья — результат интеграции обоих шлюзов в один проект. Без маркетинговой обёртки. API, webhook, комиссии, подводные камни, код на Kotlin/Spring Boot.
Сравнительная таблица: 10 критериев
| Критерий | ЮKassa | Stripe |
|---|---|---|
| API-дизайн | REST, JSON. Idempotency-Key обязателен | REST, JSON. Idempotency-Key опциональный |
| SDK | Официальный Java SDK (yookassa-sdk-java) | stripe-java, отличное качество |
| Комиссия (карты РФ) | 2.8% (Visa/MC/Мир) | Не работает с картами РФ |
| Комиссия (международные) | 3.5% + конвертация | 2.9% + $0.30 |
| Рекуррентные платежи | Есть, через сохранение payment_method | Subscriptions API, полноценный биллинг |
| Webhook | HTTP POST, retry 24 часа, до 10 попыток | HTTP POST, retry 72 часа, до 25 попыток |
| Подпись webhook | HMAC-SHA256 | HMAC-SHA256 (Stripe-Signature header) |
| Sandbox | Тестовый магазин с тестовыми картами | Полноценный test mode, отдельный API-ключ |
| Документация | Русский. Местами устаревшая | Английский. Эталон индустрии |
| 54-ФЗ (онлайн-касса) | Встроенная поддержка чеков | Нет (не нужно за пределами РФ) |
Дальше — разбор каждого пункта с кодом.
API-дизайн: REST, идемпотентность, ошибки
ЮKassa
API ЮKassa — REST поверх HTTPS. Аутентификация — HTTP Basic (shopId + секретный ключ). Все мутирующие запросы требуют заголовок Idempotency-Key. Без него API вернёт 400. Это раздражает на старте, но спасает в проде: повторная отправка из-за таймаута не создаст дубль платежа.
Создание платежа:
@Service
class YooKassaPaymentService(
private val restClient: RestClient,
@Value("\${yookassa.shop-id}") private val shopId: String,
@Value("\${yookassa.secret-key}") private val secretKey: String
) {
private val baseUrl = "https://api.yookassa.ru/v3"
fun createPayment(amount: BigDecimal, currency: String, returnUrl: String): PaymentResponse {
val idempotencyKey = UUID.randomUUID().toString()
val body = mapOf(
"amount" to mapOf(
"value" to amount.toPlainString(),
"currency" to currency
),
"confirmation" to mapOf(
"type" to "redirect",
"return_url" to returnUrl
),
"capture" to true,
"description" to "Заказ #${System.currentTimeMillis()}"
)
return restClient.post()
.uri("$baseUrl/payments")
.header("Idempotency-Key", idempotencyKey)
.headers { it.setBasicAuth(shopId, secretKey) }
.contentType(MediaType.APPLICATION_JSON)
.body(body)
.retrieve()
.body(PaymentResponse::class.java)
?: throw PaymentException("Пустой ответ от ЮKassa")
}
}
Формат ошибок — JSON с полями type, id, code, description. Коды ошибок документированы, но сообщения иногда приходят на русском, иногда на английском. В одном и том же ответе. Мелочь, но при логировании путает.
Stripe
Stripe использует тот же REST + JSON, но API-ключ передаётся через Bearer-токен. Idempotency-Key опциональный — можно не ставить, и повторный запрос создаст новый объект. На тестовом окружении это удобно, в продакшене — ловушка. Мы всегда ставим.
@Service
class StripePaymentService(
@Value("\${stripe.secret-key}") private val secretKey: String
) {
init {
Stripe.apiKey = secretKey
}
fun createPaymentIntent(amount: Long, currency: String): PaymentIntent {
val params = PaymentIntentCreateParams.builder()
.setAmount(amount) // в минимальных единицах: копейки, центы
.setCurrency(currency)
.setAutomaticPaymentMethods(
PaymentIntentCreateParams.AutomaticPaymentMethods.builder()
.setEnabled(true)
.build()
)
.build()
val requestOptions = RequestOptions.builder()
.setIdempotencyKey(UUID.randomUUID().toString())
.build()
return PaymentIntent.create(params, requestOptions)
}
}
Разница, которая бросается в глаза: Stripe SDK типизирован через builder-pattern. Каждый параметр — это метод. IDE подсказывает, компилятор ловит опечатки. В ЮKassa SDK для Java ситуация хуже — многие поля принимают Object, и ошибку вы увидите только в рантайме.
Обработка ошибок
Stripe возвращает структурированные ошибки с type (card_error, invalid_request_error, api_error), code, message, param. Каждое поле предсказуемо. Парсить удобно.
ЮKassa тоже возвращает структурированный JSON, но набор кодов меньше. Ошибка insufficient_funds от карты и ошибка card_expired обе приходят с HTTP 400 — различить можно только по code внутри тела. На практике это не проблема, но логирование нужно строить аккуратнее.
Вердикт по API: Stripe — лучше. Типизация, предсказуемость, ergonomics SDK. ЮKassa работает, но требует больше ручной обработки крайних случаев.
Если вы строите платёжную систему на Spring Boot — API-дизайн Stripe позволяет писать меньше boilerplate-кода.
Webhook: надёжность, ретраи, проверка подписи
Оба шлюза уведомляют о событиях через HTTP POST на ваш endpoint. Разница — в деталях.
Retry-политика
ЮKassa: до 10 попыток в течение 24 часов. Интервалы нарастают: 1 мин, 5 мин, 15 мин и так далее. Если ваш сервер вернул не 200 — повтор. Если вернул 200, но позже платёж отменился — новое уведомление.
Stripe: до 25 попыток в течение 72 часов. Экспоненциальный backoff. Плюс есть dashboard, где видно каждую попытку, тело запроса и ответ вашего сервера. Для дебага это бесценно — в ЮKassa такой панели нет.
Верификация подписи
Оба используют HMAC. Но формат различается.
ЮKassa webhook handler:
@RestController
@RequestMapping("/api/webhooks/yookassa")
class YooKassaWebhookController(
private val paymentService: PaymentProcessingService,
@Value("\${yookassa.webhook-secret}") private val webhookSecret: String
) {
private val objectMapper = ObjectMapper()
@PostMapping
fun handleWebhook(
@RequestBody body: String,
@RequestHeader("Content-Type") contentType: String
): ResponseEntity<Void> {
// ЮKassa не отправляет подпись в заголовке по умолчанию.
// Верификация — через IP-whitelist или проверку объекта через API.
// Если включена подпись (новый формат), проверяем так:
val event = objectMapper.readTree(body)
val eventType = event["event"].asText()
val paymentId = event["object"]["id"].asText()
// Перепроверяем статус через API — золотое правило
val payment = paymentService.fetchPaymentFromApi(paymentId)
when (eventType) {
"payment.succeeded" -> paymentService.handleSuccess(payment)
"payment.canceled" -> paymentService.handleCancellation(payment)
"refund.succeeded" -> paymentService.handleRefund(payment)
else -> log.warn("Неизвестный тип события: $eventType")
}
return ResponseEntity.ok().build()
}
}
Важный момент: в ЮKassa лучше всегда перепроверять статус платежа через API после получения webhook. Документация сама это рекомендует. Это добавляет один HTTP-вызов, но защищает от подделки уведомлений.
Stripe webhook handler:
@RestController
@RequestMapping("/api/webhooks/stripe")
class StripeWebhookController(
private val paymentService: PaymentProcessingService,
@Value("\${stripe.webhook-secret}") private val endpointSecret: String
) {
@PostMapping
fun handleWebhook(
@RequestBody payload: String,
@RequestHeader("Stripe-Signature") sigHeader: String
): ResponseEntity<String> {
val event: Event = try {
Webhook.constructEvent(payload, sigHeader, endpointSecret)
} catch (e: SignatureVerificationException) {
log.error("Подпись webhook невалидна", e)
return ResponseEntity.badRequest().body("Invalid signature")
}
when (event.type) {
"payment_intent.succeeded" -> {
val paymentIntent = event.dataObjectDeserializer
.`object`.orElseThrow() as PaymentIntent
paymentService.handleStripeSuccess(paymentIntent)
}
"payment_intent.payment_failed" -> {
val paymentIntent = event.dataObjectDeserializer
.`object`.orElseThrow() as PaymentIntent
paymentService.handleStripeFailure(paymentIntent)
}
else -> log.info("Необработанное событие: ${event.type}")
}
return ResponseEntity.ok("OK")
}
}
У Stripe подпись криптографически привязана к телу запроса. Подделать нельзя без знания секрета. У ЮKassa старый формат (IP-whitelist + перепроверка через API) менее элегантен, но тоже работает.
Вердикт по webhook: Stripe — значительно удобнее. Retry-панель, подпись в заголовке, 72 часа на доставку. ЮKassa функционально достаточна, но дебаг сложнее.
Рекуррентные платежи и подписки
Здесь разрыв максимальный.
Stripe Billing
Stripe Billing — это полноценная система подписок. Создаёте Product, добавляете Price (monthly/yearly/usage-based), привязываете Customer, создаёте Subscription. Автоматические попытки списания, grace-period, proration при смене тарифа, купоны, trials — всё из коробки.
Для SaaS-проекта это экономит 2-3 месяца разработки. Вместо того чтобы писать биллинг с нуля, вы конфигурируете его через API или dashboard.
ЮKassa
В ЮKassa рекуррентные платежи работают иначе. Вы сохраняете payment_method_id после первого платежа (с флагом save_payment_method: true) и затем инициируете повторные списания через API. Весь биллинговый движок — расписание, retry-логику, proration, grace-period — пишете сами.
// Повторное списание по сохранённому методу в ЮKassa
fun chargeRecurring(paymentMethodId: String, amount: BigDecimal): PaymentResponse {
val body = mapOf(
"amount" to mapOf(
"value" to amount.toPlainString(),
"currency" to "RUB"
),
"payment_method_id" to paymentMethodId,
"capture" to true,
"description" to "Автоматическое продление подписки"
)
return restClient.post()
.uri("$baseUrl/payments")
.header("Idempotency-Key", UUID.randomUUID().toString())
.headers { it.setBasicAuth(shopId, secretKey) }
.contentType(MediaType.APPLICATION_JSON)
.body(body)
.retrieve()
.body(PaymentResponse::class.java)
?: throw PaymentException("Ошибка рекуррентного платежа")
}
Это не плохо — это просто другой подход. Вы получаете полный контроль. Но если у вас 10 тарифных планов с trial-периодами и годовой скидкой, приготовьтесь написать 3-5 тысяч строк биллинговой логики. Или закажите разработку финтех-системы — мы такие вещи собирали для нескольких e-commerce проектов.
Вердикт по подпискам: Stripe — вне конкуренции для SaaS-биллинга. ЮKassa — рабочий инструмент для рублёвых подписок, но без готового subscription engine.
Комиссии: считаем в деньгах
Комиссии — то, что решает выбор шлюза не реже, чем API.
ЮKassa (май 2026)
| Метод оплаты | Комиссия |
|---|---|
| Банковские карты (Visa, MC, Мир) | 2.8% |
| SBP (Система быстрых платежей) | 0.4–0.7% |
| ЮMoney (кошелёк) | 3.0% |
| Apple Pay / Google Pay | 2.8% |
| Рассрочка (Сплит) | 3.5–5.0% |
SBP заслуживает отдельного упоминания. 0.4% — это в 7 раз дешевле карт. Для среднего чека 5 000₽ разница: 140₽ (карта) против 20₽ (SBP). На 10 000 транзакций в месяц — 1.2 млн рублей экономии в год. Если ваша аудитория в России, SBP через ЮKassa — самый дешёвый канал приёма.
Stripe (май 2026)
| Регион | Комиссия |
|---|---|
| Стандарт (EU/US) | 2.9% + $0.30 |
| Европейские карты (для EU-бизнесов) | 1.5% + €0.25 |
| Конвертация валюты | +1% |
| Повторные списания (Billing) | 0.5% (доп.) |
| Stripe Tax | 0.5% (доп.) |
Для международных платежей Stripe дешевле, если ваше юрлицо в ЕС. Для транзакций US→US стандартные 2.9% + $0.30 — на уровне рынка.
Сценарий: средний чек 3 000₽
- ЮKassa (карта): 84₽ → вы получаете 2 916₽
- ЮKassa (SBP): 12₽ → вы получаете 2 988₽
- Stripe (2.9% + $0.30, если бы работал в РФ): ~117₽ → получили бы 2 883₽
Вердикт по комиссиям: Для рублёвых платежей ЮKassa дешевле. SBP — на порядок дешевле карт. Для международных — Stripe стандартен и прозрачен.
Документация и SDK
Вопрос, который вызывает у разработчиков самую яркую реакцию.
Stripe
Документация Stripe — золотой стандарт. Серьёзно. Каждый endpoint описан с примерами на 7 языках. Есть интерактивный API explorer, где можно выполнить запрос прямо из браузера. Примеры кода — рабочие, а не «примерные». Гайды по интеграции — пошаговые, с проверочными шагами.
Java/Kotlin SDK типизирован, поддерживает builder-pattern, обновляется в день выхода нового API-фичи. Maven-артефакт com.stripe:stripe-java — 500+ звёзд на GitHub, 12 000+ коммитов.
ЮKassa
Документация ЮKassa... функциональна. Всё нужное есть: описание endpoints, параметры, коды ошибок. Но:
- Примеры кода — только PHP и Python. Java/Kotlin — ищите сами.
- Гайд по интеграции — один длинный документ без пошагового flow.
- API changelog ведётся нерегулярно. Мы дважды сталкивались с тем, что поведение API менялось без обновления документации.
- Java SDK (
yookassa-sdk-java) существует, но обновляется реже, чем хотелось бы.
Это не катастрофа. Опытный Java-разработчик разберётся за день. Но джуниор потратит неделю и набьёт шишки.
Вердикт по документации: Stripe — лучшая документация из всех платёжных систем мира. ЮKassa — приемлемая, но заметно уступает.
Тестовое окружение и sandbox
Stripe
Два API-ключа: live и test. В тестовом режиме можно создавать платежи, подписки, webhook — всё идентично продакшену, но деньги не списываются. Тестовые карты: 4242 4242 4242 4242 (успех), 4000 0000 0000 0002 (отказ), десятки сценариев (3D Secure, недостаток средств, expired).
Dashboard показывает тестовые события наравне с боевыми (переключатель Test/Live). Webhook-лог с телами запросов и ответов.
ЮKassa
Тестовый магазин создаётся в личном кабинете. Тестовые карты документированы, основные сценарии покрыты. Но:
- Тестовый shopId и боевой — разные. При деплое легко перепутать, если не вынести в env-переменные.
- Webhook в тестовом режиме работает, но иногда задерживается на 5-10 минут (в продакшене — секунды).
- Нет аналога Stripe CLI (
stripe listen --forward-to localhost:8080) для локальной разработки. Нужен ngrok или аналог.
Вердикт: Stripe — эталон. ЮKassa — работает, но нет инструментов для локальной разработки.
Регуляторные требования: 54-ФЗ, 152-ФЗ, PCI DSS
Если вы принимаете платежи в России, вы обязаны соблюдать 54-ФЗ (онлайн-кассы). Каждая транзакция требует фискальный чек.
ЮKassa + 54-ФЗ
ЮKassa интегрируется с кассовыми решениями (АТОЛ, Эвотор, Модулькасса) и умеет отправлять чеки автоматически. В API передаёте объект receipt с позициями, ставками НДС, email покупателя — ЮKassa формирует и отправляет чек в ОФД. Одной заботой меньше.
val receipt = mapOf(
"customer" to mapOf("email" to "buyer@example.com"),
"items" to listOf(
mapOf(
"description" to "Подписка Premium (1 мес.)",
"quantity" to "1.00",
"amount" to mapOf("value" to "990.00", "currency" to "RUB"),
"vat_code" to 2, // НДС 20%
"payment_subject" to "service",
"payment_mode" to "full_payment"
)
)
)
Stripe + российское законодательство
Stripe не работает с российскими юрлицами и картами. Вопрос 54-ФЗ не стоит. Но если у вас двойная структура (российское юрлицо для РФ + зарубежное для мира), Stripe не поможет с фискализацией — придётся интегрировать ОФД отдельно.
PCI DSS
Оба шлюза — PCI DSS Level 1. Карточные данные не касаются вашего сервера: ЮKassa использует iframe/redirect, Stripe — Stripe.js + Elements. Ваш бэкенд работает только с токенами.
152-ФЗ (персональные данные)
ЮKassa хранит данные на территории РФ — 152-ФЗ соблюдается автоматически. Stripe — серверы в США и ЕС. Для российских пользователей это формально нарушение, но при международных платежах (зарубежное юрлицо) — допустимо.
Вердикт: Для работы в российском правовом поле ЮKassa — единственный корректный вариант. Stripe — для международных операций через зарубежное юрлицо.
Альтернативы: CloudPayments, Robokassa, T-Pay
ЮKassa и Stripe — не единственные. Коротко о трёх альтернативах, которые мы тоже интегрировали:
CloudPayments
- Комиссия: 2.5–2.7% (карты), от 0.4% (SBP)
- Сильная сторона: рекуррентные платежи. Subscription API ближе к Stripe, чем у ЮKassa. Есть Apple Pay, Google Pay, SBP. Widget встраивается в iframe.
- Слабая сторона: документация слабее ЮKassa. SDK для Java нет — только REST.
- Когда выбирать: если нужен SaaS-биллинг в рублях и не хотите писать scheduler самостоятельно.
Robokassa
- Комиссия: 3.5–5.0% (зависит от метода)
- Сильная сторона: простота подключения. Регистрация — 15 минут, интеграция — redirect на их страницу. Принимает десятки методов: карты, кошельки, терминалы.
- Слабая сторона: высокая комиссия, устаревший API (XML-based для некоторых методов), минимальные webhook-возможности.
- Когда выбирать: MVP, который нужно запустить за день. Не для highload.
T-Pay (Тинькофф)
- Комиссия: 2.49–2.79% (карты), 0.4% (SBP)
- Сильная сторона: SBP по QR с низкой комиссией. Хорошая документация (лучше ЮKassa, на уровне CloudPayments). REST API, внятные ошибки.
- Слабая сторона: меньше методов оплаты, чем у ЮKassa. Нет рассрочки.
- Когда выбирать: если основной банк клиентов — Тинькофф и важен SBP.
Для заказной разработки мы обычно рекомендуем ЮKassa как основной шлюз (широта методов + 54-ФЗ) и CloudPayments как запасной — особенно если есть подписочная модель.
Архитектура: multi-gateway pattern
Если проект работает с двумя и более шлюзами, имеет смысл выделить абстрактный интерфейс. Вот паттерн, который мы используем:
interface PaymentGateway {
val name: String
fun createPayment(request: PaymentRequest): PaymentResult
fun capturePayment(paymentId: String): PaymentResult
fun refundPayment(paymentId: String, amount: BigDecimal?): RefundResult
fun verifyWebhook(payload: String, headers: Map<String, String>): WebhookEvent?
}
data class PaymentRequest(
val amount: BigDecimal,
val currency: String,
val returnUrl: String,
val description: String,
val metadata: Map<String, String> = emptyMap(),
val savePaymentMethod: Boolean = false,
val receiptItems: List<ReceiptItem>? = null // для 54-ФЗ
)
data class PaymentResult(
val gatewayPaymentId: String,
val status: PaymentStatus,
val confirmationUrl: String?,
val rawResponse: String
)
enum class PaymentStatus {
PENDING, SUCCEEDED, CANCELED, REFUNDED, WAITING_FOR_CAPTURE
}
Реализация для ЮKassa:
@Service
class YooKassaGateway(
private val restClient: RestClient,
private val config: YooKassaProperties
) : PaymentGateway {
override val name = "yookassa"
override fun createPayment(request: PaymentRequest): PaymentResult {
val body = buildYooKassaPayload(request)
val response = restClient.post()
.uri("${config.baseUrl}/payments")
.header("Idempotency-Key", UUID.randomUUID().toString())
.headers { it.setBasicAuth(config.shopId, config.secretKey) }
.contentType(MediaType.APPLICATION_JSON)
.body(body)
.retrieve()
.body(JsonNode::class.java)!!
return PaymentResult(
gatewayPaymentId = response["id"].asText(),
status = mapYooKassaStatus(response["status"].asText()),
confirmationUrl = response["confirmation"]?.get("confirmation_url")?.asText(),
rawResponse = response.toString()
)
}
override fun verifyWebhook(payload: String, headers: Map<String, String>): WebhookEvent? {
val json = objectMapper.readTree(payload)
val paymentId = json["object"]["id"].asText()
// Перепроверяем через API
val verified = fetchPayment(paymentId)
return WebhookEvent(
type = json["event"].asText(),
paymentId = paymentId,
status = mapYooKassaStatus(verified["status"].asText())
)
}
// capturePayment, refundPayment — аналогично
}
Реализация для Stripe:
@Service
class StripeGateway(
private val config: StripeProperties
) : PaymentGateway {
override val name = "stripe"
init { Stripe.apiKey = config.secretKey }
override fun createPayment(request: PaymentRequest): PaymentResult {
val params = PaymentIntentCreateParams.builder()
.setAmount(request.amount.multiply(BigDecimal(100)).toLong())
.setCurrency(request.currency.lowercase())
.putAllMetadata(request.metadata)
.setAutomaticPaymentMethods(
PaymentIntentCreateParams.AutomaticPaymentMethods.builder()
.setEnabled(true)
.build()
)
.build()
val intent = PaymentIntent.create(params, requestOptions())
return PaymentResult(
gatewayPaymentId = intent.id,
status = mapStripeStatus(intent.status),
confirmationUrl = intent.clientSecret, // для Stripe.js
rawResponse = intent.toJson()
)
}
override fun verifyWebhook(payload: String, headers: Map<String, String>): WebhookEvent? {
val sigHeader = headers["Stripe-Signature"] ?: return null
val event = Webhook.constructEvent(payload, sigHeader, config.webhookSecret)
val intent = event.dataObjectDeserializer.`object`.orElse(null) as? PaymentIntent
?: return null
return WebhookEvent(
type = event.type,
paymentId = intent.id,
status = mapStripeStatus(intent.status)
)
}
}
Роутинг между шлюзами:
@Service
class PaymentRouter(
private val gateways: List<PaymentGateway>
) {
private val gatewayMap = gateways.associateBy { it.name }
fun route(request: PaymentRequest): PaymentGateway {
return when (request.currency.uppercase()) {
"RUB" -> gatewayMap["yookassa"]
?: throw IllegalStateException("ЮKassa gateway не сконфигурирован")
else -> gatewayMap["stripe"]
?: throw IllegalStateException("Stripe gateway не сконфигурирован")
}
}
}
Этот паттерн позволяет добавить третий шлюз (CloudPayments, T-Pay) за один рабочий день: реализуете интерфейс, добавляете правило в route(), тестируете.
Если вам нужна подобная архитектура в проде — мы проектируем такие вещи для enterprise-проектов на Java/Kotlin стеке.
Матрица принятия решений
| Ситуация | Рекомендация | Почему |
|---|---|---|
| SaaS, аудитория — РФ | ЮKassa + свой биллинг | SBP дёшево, 54-ФЗ из коробки |
| SaaS, международная аудитория | Stripe Billing | Подписки из коробки, 30+ валют |
| E-commerce, РФ + СНГ | ЮKassa (основной) + CloudPayments (резерв) | Широта методов + fallback |
| E-commerce, РФ + Европа | ЮKassa (РФ) + Stripe (мир), multi-gateway | Каждый шлюз в своей юрисдикции |
| MVP, нужно за 2 дня | Robokassa | Redirect-интеграция, запуск за часы |
| Маркетплейс с выплатами продавцам | Stripe Connect (международный) / ЮKassa Split (РФ) | Встроенная логика split-платежей |
Если ваш случай не укладывается в таблицу — читайте наш обзор платёжных систем с разбором по нишам.
Итого: что мы выбираем для клиентских проектов
За 2025–2026 год мы интегрировали ЮKassa в 6 проектов и Stripe в 3. В двух проектах — оба одновременно через multi-gateway pattern.
Если клиент спрашивает «что поставить» — ответ зависит от юрисдикции и бизнес-модели, а не от «какой API красивее». Stripe лучше по инженерным критериям: документация, SDK, инструменты для разработчика. ЮKassa — обязательный выбор для российского рынка: 54-ФЗ, SBP, карта Мир, рублёвые комиссии.
Правильный вопрос — не «ЮKassa или Stripe», а «какую платёжную архитектуру строить, чтобы через полгода не переписывать». Абстрактный PaymentGateway интерфейс, грамотная обработка webhook, idempotency на каждом шаге. Если это звучит как ваш проект — напишите нам, обсудим архитектуру.