Якщо LiqPay та Monobank — рішення для українського ринку, то Stripe — рішення для міжнародного масштабування. Stripe обробляє платежі більш як у 135 країнах і є de facto стандартом у глобальній SaaS-індустрії.
З 2022 року Stripe став офіційно доступним для ФОП та юридичних осіб, зареєстрованих в Україні. Для стартапів, що планують вихід на міжнародний ринок, або для SaaS-компаній з іноземними клієнтами — це обов'язкова інтеграція.
Ключові переваги Stripe для розробника:
stripe-dotnet — офіційний, добре підтримуваний NuGet-пакетdotnet add package Stripe.net
Publishable key та Secret keyВстановіть Stripe CLI для локального тестування:
# Windows (winget)
winget install Stripe.StripeCLI
# або завантажте з github.com/stripe/stripe-cli/releases
{
"Stripe": {
"SecretKey": "sk_test_...",
"PublishableKey": "pk_test_...",
"WebhookSecret": "whsec_...",
"Currency": "uah"
}
}
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";
}
// 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);
});
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.
StripePaymentProviderusing 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
};
}
Stripe надає зручний вбудований інструмент верифікації підпису webhook — на відміну від LiqPay та Monobank, де ми реалізували верифікацію вручну.
// Додайте до 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 listen --forward-to localhost:5001/webhooks/stripe
| Критерій | LiqPay | Monobank | Stripe |
|---|---|---|---|
| Цільовий ринок | Україна | Україна | Весь світ |
| Автентифікація запитів | HMAC-SHA1 підпис | Bearer токен | Bearer токен |
| Webhook підпис | HMAC-SHA1 | ECDSA P-256 | HMAC-SHA256 |
| SDK для .NET | Немає (HTTP вручну) | Немає (HTTP вручну) | Stripe.net |
| Тестування webhook | ngrok | ngrok | Stripe 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: Запустіть демо з Stripe Checkout у Sandbox режимі. Використайте тестову картку 4242 4242 4242 4242 та перевірте, що webhook отримано і статус оновлено.
Завдання 1.2: Знайдіть у Stripe документації список decline codes та поясніть, що означають: insufficient_funds, card_declined, expired_card, incorrect_cvc.
Завдання 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.1: Спроєктуйте систему smart routing: для транзакцій до 1000 грн з UA картками → Monobank (нижча комісія), для решти UA транзакцій → LiqPay, для іноземних карток → Stripe. Реалізуйте SmartRoutingPaymentProviderFactory з конфігурованими правилами через appsettings.json.
Інтеграція Monobank Acquiring API
Покрокова інтеграція Monobank Acquiring — Invoice API, QR-оплата, ECDSA верифікація webhook та реалізація MonobankPaymentProvider.
Webhooks — глибоке занурення
Детальний розбір механіки Webhooks для платіжних систем — асинхронний life-cycle, верифікація підписів, ідемпотентність, retry-логіка та Channel-based обробка в ASP.NET.