Платежі

Інтеграція Stripe

Stripe як міжнародний стандарт платіжних інтеграцій — Payment Intents API, Stripe.NET SDK, Stripe Checkout, обробка webhook-подій та порівняння з LiqPay і Monobank.

Інтеграція Stripe

Чому Stripe — це золотий стандарт

Якщо LiqPay та Monobank — рішення для українського ринку, то Stripe — рішення для міжнародного масштабування. Stripe обробляє платежі більш як у 135 країнах і є de facto стандартом у глобальній SaaS-індустрії.

З 2022 року Stripe став офіційно доступним для ФОП та юридичних осіб, зареєстрованих в Україні. Для стартапів, що планують вихід на міжнародний ринок, або для SaaS-компаній з іноземними клієнтами — це обов'язкова інтеграція.

Ключові переваги Stripe для розробника:

  • Найкраща документація та DX у галузі
  • stripe-dotnet — офіційний, добре підтримуваний NuGet-пакет
  • Stripe CLI для локального тестування webhook без ngrok
  • Вбудовані підписки, інвойси, coupon-система
  • Payment Intents API — гнучкий flow з підтримкою 3DS

Крок 0: Підготовка

Встановлення NuGet-пакету

dotnet add package Stripe.net

Отримання ключів

  1. Зареєструйтесь на dashboard.stripe.com
  2. Перейдіть у DevelopersAPI keys
  3. Скопіюйте Publishable key та Secret key

Stripe CLI для webhook

Встановіть Stripe CLI для локального тестування:

# Windows (winget)
winget install Stripe.StripeCLI

# або завантажте з github.com/stripe/stripe-cli/releases

Крок 1: Конфігурація

appsettings.Development.json
{
  "Stripe": {
    "SecretKey": "sk_test_...",
    "PublishableKey": "pk_test_...",
    "WebhookSecret": "whsec_...",
    "Currency": "uah"
  }
}
Options/StripeOptions.cs
public class StripeOptions
{
    public string SecretKey { get; set; } = null!;
    public string PublishableKey { get; set; } = null!;
    public string WebhookSecret { get; set; } = null!;
    public string Currency { get; set; } = "uah";
}
Program.cs
// Stripe SDK ініціалізація через глобальний клас
builder.Services.Configure<StripeOptions>(
    builder.Configuration.GetSection("Stripe"));

builder.Services.AddScoped<StripePaymentProvider>(sp =>
{
    var opts = sp.GetRequiredService<IOptions<StripeOptions>>().Value;
    StripeConfiguration.ApiKey = opts.SecretKey;
    return new StripePaymentProvider(opts);
});

Крок 2: Payment Intents API

Stripe побудований навколо концепту Payment Intent (намір оплати). Це об'єкт, що описує транзакцію та її поточний стан.

На відміну від LiqPay (де ми генеруємо посилання самостійно) або Monobank (де ми отримуємо pageUrl), zі Stripe є два основних flow:

Stripe Checkout (Hosted Page) — найпростіший спосіб. Stripe повністю управляє сторінкою оплати. Мінімум коду, SAQ-A відповідність.

Payment Intents + Elements — гнучкий спосіб. Ваш фронтенд відображає вбудовану форму від Stripe, ваш бекенд управляє Payment Intent.

Для нашого прикладу (Minimal API + простий фронтенд) ми використаємо Stripe Checkout.


Крок 3: Реалізація StripePaymentProvider

Providers/Stripe/StripePaymentProvider.cs
using Stripe;
using Stripe.Checkout;

public class StripePaymentProvider : IPaymentProvider
{
    private readonly StripeOptions _options;

    public string ProviderName => "stripe";

    public StripePaymentProvider(StripeOptions options)
        => _options = options;

    public async Task<CreatePaymentResult> CreatePaymentAsync(
        PaymentRequest request,
        CancellationToken ct = default)
    {
        // Stripe Checkout Session — найпростіший hosted flow
        var sessionOptions = new SessionCreateOptions
        {
            PaymentMethodTypes = ["card"],
            LineItems =
            [
                new SessionLineItemOptions
                {
                    PriceData = new SessionLineItemPriceDataOptions
                    {
                        UnitAmount = (long)(request.Amount * 100), // Копійки/центи
                        Currency = request.Currency.ToLower(),
                        ProductData = new SessionLineItemPriceDataProductDataOptions
                        {
                            Name = request.Description
                        }
                    },
                    Quantity = 1
                }
            ],
            Mode = "payment",
            SuccessUrl = request.ReturnUrl + "?session_id={CHECKOUT_SESSION_ID}",
            CancelUrl = request.ReturnUrl + "?cancelled=true",

            // Ключ ідемпотентності для уникнення дублювання
            ClientReferenceId = request.IdempotencyKey
        };

        var service = new SessionService();
        var session = await service.CreateAsync(sessionOptions,
            requestOptions: new RequestOptions
            {
                IdempotencyKey = request.IdempotencyKey
            },
            cancellationToken: ct);

        return new CreatePaymentResult(
            Success: true,
            CheckoutUrl: session.Url,            // Redirect на stripe.com/checkout
            FormData: session.Id,               // Session ID для перевірки статусу
            ProviderPaymentId: session.Id,
            ErrorMessage: null
        );
    }

    public async Task<PaymentStatusResult> GetPaymentStatusAsync(
        string providerTransactionId,
        CancellationToken ct = default)
    {
        var service = new SessionService();
        var session = await service.GetAsync(providerTransactionId,
            cancellationToken: ct);

        return new PaymentStatusResult(
            Success: true,
            Status: MapStripeStatus(session.PaymentStatus),
            ProviderTransactionId: session.PaymentIntentId,
            ErrorMessage: null
        );
    }

    public async Task<RefundResult> RefundAsync(
        string providerTransactionId,
        decimal amount,
        CancellationToken ct = default)
    {
        // providerTransactionId = PaymentIntent ID (pi_...)
        var refundOptions = new RefundCreateOptions
        {
            PaymentIntent = providerTransactionId,
            Amount = (long)(amount * 100)
        };

        var service = new RefundService();
        var refund = await service.CreateAsync(refundOptions, cancellationToken: ct);

        return refund.Status == "succeeded"
            ? new RefundResult(true, refund.Id, null)
            : new RefundResult(false, null, refund.FailureReason);
    }

    public Task<WebhookResult> ProcessWebhookAsync(
        HttpRequest httpRequest,
        CancellationToken ct = default)
        => throw new NotSupportedException("Use WebhookEndpoints for Stripe webhook");

    private static PaymentStatus MapStripeStatus(string? status) =>
        status switch
        {
            "paid"       => PaymentStatus.Settled,
            "unpaid"     => PaymentStatus.Pending,
            "no_payment_required" => PaymentStatus.Settled,
            _ => PaymentStatus.Pending
        };
}

Крок 4: Stripe Webhook з вбудованою верифікацією

Stripe надає зручний вбудований інструмент верифікації підпису webhook — на відміну від LiqPay та Monobank, де ми реалізували верифікацію вручну.

Endpoints/WebhookEndpoints.cs
// Додайте до MapWebhookEndpoints:
app.MapPost("/webhooks/stripe", HandleStripeWebhook)
    .WithTags("Webhooks");

private static async Task<IResult> HandleStripeWebhook(
    HttpRequest request,
    IOptions<StripeOptions> options,
    PaymentService paymentService,
    ILogger<Program> logger,
    CancellationToken ct)
{
    // Зчитуємо сире тіло (необхідно до будь-якої обробки)
    var body = await new StreamReader(request.Body).ReadToEndAsync(ct);
    var stripeSignature = request.Headers["Stripe-Signature"].ToString();

    // Stripe SDK верифікує підпис та парсить event — одна строчка!
    Event stripeEvent;
    try
    {
        stripeEvent = EventUtility.ConstructEvent(
            body, stripeSignature, options.Value.WebhookSecret);
    }
    catch (StripeException ex)
    {
        logger.LogWarning("Stripe webhook signature verification failed: {Error}", ex.Message);
        return Results.BadRequest("Invalid signature");
    }

    logger.LogInformation("Stripe webhook event: {Type}", stripeEvent.Type);

    // Обробляємо конкретні типи подій
    switch (stripeEvent.Type)
    {
        case Events.CheckoutSessionCompleted:
        {
            var session = stripeEvent.Data.Object as Session;
            if (session?.PaymentStatus == "paid")
            {
                await paymentService.UpdatePaymentByProviderIdAsync(
                    providerTransactionId: session.Id,
                    newStatus: PaymentStatus.Settled,
                    rawPayload: body);
            }
            break;
        }

        case Events.PaymentIntentPaymentFailed:
        {
            var pi = stripeEvent.Data.Object as PaymentIntent;
            if (pi is not null)
            {
                await paymentService.UpdatePaymentByProviderIdAsync(
                    providerTransactionId: pi.Id,
                    newStatus: PaymentStatus.Failed,
                    rawPayload: body);
            }
            break;
        }

        case Events.ChargeRefunded:
        {
            var charge = stripeEvent.Data.Object as Charge;
            if (charge is not null)
            {
                await paymentService.UpdatePaymentByProviderIdAsync(
                    providerTransactionId: charge.PaymentIntentId,
                    newStatus: PaymentStatus.Refunded,
                    rawPayload: body);
            }
            break;
        }
    }

    return Results.Ok();
}
Stripe CLI для локального тестування webhook: Запустіть у окремому терміналі:
stripe listen --forward-to localhost:5001/webhooks/stripe
Stripe CLI автоматично підпише всі webhook-запити та перешле на ваш локальний сервер. Не потрібен ngrok!

Порівняння: LiqPay vs Monobank vs Stripe

КритерійLiqPayMonobankStripe
Цільовий ринокУкраїнаУкраїнаВесь світ
Автентифікація запитівHMAC-SHA1 підписBearer токенBearer токен
Webhook підписHMAC-SHA1ECDSA P-256HMAC-SHA256
SDK для .NETНемає (HTTP вручну)Немає (HTTP вручну)Stripe.net
Тестування webhookngrokngrokStripe CLI (вбудований)
ДокументаціяСередняХорошаВідмінна
ПідпискиОбмеженоНемаєПовноцінний Subscriptions API
Тарифи (MDR)~1.5–2.5%~1.2–2%~2.9% + $0.30
Виплати в УкраїнуАвтоматичноАвтоматичноПотребує реєстрації
СумаГривні (decimal)Копійки (long)Найменші одиниці (long)

Підсумок

Stripe вирізняється найкращим досвідом розробника: офіційний SDK, вбудована верифікація webhook, Payment Intents API та повноцінна система підписок. Ідеально підходить для SaaS-продуктів та міжнародного масштабування. Для суто українського ринку LiqPay та Monobank матимуть кращі тарифи та більшу конверсію (звичні бренди). Оптимальна стратегія — підтримувати всіх трьох провайдерів через IPaymentProvider-абстракцію та обирати залежно від аудиторії та контексту платежу.

Практичні завдання

Рівень 1

Завдання 1.1: Запустіть демо з Stripe Checkout у Sandbox режимі. Використайте тестову картку 4242 4242 4242 4242 та перевірте, що webhook отримано і статус оновлено.

Завдання 1.2: Знайдіть у Stripe документації список decline codes та поясніть, що означають: insufficient_funds, card_declined, expired_card, incorrect_cvc.

Рівень 2

Завдання 2.1: Додайте до StripePaymentProvider.CreatePaymentAsync підтримку metadata — передайте у Stripe Session Metadata["order_id"] та Metadata["customer_email"]. Як ця metadata повернеться у webhook?

Завдання 2.2: Реалізуйте метод StripePaymentProvider.CreatePaymentIntentAsync (замість Checkout Session) з підтримкою confirm: false та отриманням client_secret для вбудованої Stripe Elements форми.

Рівень 3

Завдання 3.1: Спроєктуйте систему smart routing: для транзакцій до 1000 грн з UA картками → Monobank (нижча комісія), для решти UA транзакцій → LiqPay, для іноземних карток → Stripe. Реалізуйте SmartRoutingPaymentProviderFactory з конфігурованими правилами через appsettings.json.