Monobank — найскоріший за зростанням банк України, що має понад 9 мільйонів клієнтів (дані 2024 року). Для бізнесу це означає: значна частина ваших потенційних покупців вже є клієнтами Monobank і звикли до платежів через їхній додаток.
Monobank Acquiring — B2B-продукт банку з REST API для прийому безготівкових платежів. Серед його переваг: простота інтеграції, конкурентні тарифи та підтримка QR-інвойсів (особливо актуально для харчового бізнесу та сервісних компаній).
Monobank Acquiring доступний через business.monobank.ua. Для тестування вам потрібно отримати токен у підтримці Monobank (або через особистий кабінет після верифікації ФОП/юрособи).
Тестовий токен: Напишіть у Monobank Business підтримку з проханням надати sandbox-токен для тестування API.
Актуальна документація: api.monobank.ua/docs
{
"Monobank": {
"Token": "your_monobank_acquiring_token",
"WebhookPublicKey": "your_public_key_for_verification",
"IsSandbox": true
}
}
public class MonobankOptions
{
public string Token { get; set; } = null!;
// Публічний ключ для верифікації ECDSA-підпису webhook
public string WebhookPublicKey { get; set; } = null!;
public bool IsSandbox { get; set; }
}
Monobank Acquiring відрізняється від LiqPay за підходом до автентифікації та структурою API.
Автентифікація: Bearer-токен в заголовку X-Token. Жодних підписів для запитів — токен є єдиним ключем.
Webhook-підпис: На відміну від LiqPay (HMAC-SHA1), Monobank використовує ECDSA-підпис (Elliptic Curve Digital Signature Algorithm) на основі публічного ключа. Верифікація є складнішою, але криптографічно надійнішою.
public class MonobankClient
{
private readonly HttpClient _http;
private readonly MonobankOptions _options;
private const string BaseUrl = "https://api.monobank.ua/api/merchant";
public MonobankClient(HttpClient http, IOptions<MonobankOptions> options)
{
_http = http;
_options = options.Value;
_http.DefaultRequestHeaders.Add("X-Token", _options.Token);
_http.BaseAddress = new Uri(BaseUrl);
}
/// <summary>
/// Створює інвойс на оплату. Повертає URL та QR-код.
/// </summary>
public async Task<MonobankInvoiceResponse?> CreateInvoiceAsync(
MonobankInvoiceRequest request,
CancellationToken ct = default)
{
var response = await _http.PostAsJsonAsync("/invoice/create", request, ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MonobankInvoiceResponse>(ct);
}
/// <summary>
/// Отримує статус інвойсу за його ID.
/// </summary>
public async Task<MonobankInvoiceStatus?> GetInvoiceStatusAsync(
string invoiceId,
CancellationToken ct = default)
{
var response = await _http.GetAsync($"/invoice/status?invoiceId={invoiceId}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MonobankInvoiceStatus>(ct);
}
/// <summary>
/// Скасовує інвойс (лише до його оплати).
/// </summary>
public async Task CancelInvoiceAsync(string invoiceId, CancellationToken ct = default)
{
var response = await _http.PostAsJsonAsync("/invoice/cancel",
new { invoiceId }, ct);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Ініціює повернення коштів.
/// </summary>
public async Task<MonobankRefundResponse?> RefundAsync(
string invoiceId, decimal amount, CancellationToken ct = default)
{
var response = await _http.PostAsJsonAsync("/invoice/cancel/list",
new { invoiceList = new[] { new { invoiceId } } }, ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MonobankRefundResponse>(ct);
}
public MonobankOptions Options => _options;
}
using System.Text.Json.Serialization;
// Запит на створення інвойсу
public class MonobankInvoiceRequest
{
[JsonPropertyName("amount")]
public long Amount { get; set; } // У копійках! 100 грн = 10000
[JsonPropertyName("ccy")]
public int Ccy { get; set; } = 980; // 980 = UAH (ISO 4217)
[JsonPropertyName("merchantPaymInfo")]
public MerchantPaymInfo? MerchantPaymInfo { get; set; }
[JsonPropertyName("redirectUrl")]
public string? RedirectUrl { get; set; }
[JsonPropertyName("webHookUrl")]
public string? WebHookUrl { get; set; }
}
public class MerchantPaymInfo
{
[JsonPropertyName("reference")]
public string Reference { get; set; } = null!; // Ваш order/payment ID
[JsonPropertyName("destination")]
public string Destination { get; set; } = null!; // Опис для виписки клієнта
}
// Відповідь від API при створенні інвойсу
public record MonobankInvoiceResponse(
[property: JsonPropertyName("invoiceId")] string InvoiceId,
[property: JsonPropertyName("pageUrl")] string PageUrl, // Redirect URL
[property: JsonPropertyName("qrCode")] string? QrCode // SVG рядок QR-коду
);
// Статус інвойсу
public record MonobankInvoiceStatus(
[property: JsonPropertyName("invoiceId")] string InvoiceId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("amount")] long Amount,
[property: JsonPropertyName("ccy")] int Ccy,
[property: JsonPropertyName("reference")] string? Reference
);
public record MonobankRefundResponse(
[property: JsonPropertyName("invoiceId")] string InvoiceId,
[property: JsonPropertyName("status")] string Status
);
100 грн = 10000. Це поширена помилка при інтеграції — завжди конвертуйте decimal amount * 100 → long.Це найскладніша частина інтеграції Monobank. Замість HMAC-SHA1 (як у LiqPay), Monobank підписує webhook за допомогою ECDSA з кривою P-256 (також відомою як secp256r1).
using System.Security.Cryptography;
using System.Text;
public static class MonobankSignatureVerifier
{
/// <summary>
/// Верифікує ECDSA-підпис webhook від Monobank.
/// </summary>
/// <param name="body">Сире тіло HTTP-запиту (байти)</param>
/// <param name="signatureBase64">Підпис з заголовка X-Sign</param>
/// <param name="publicKeyBase64">Публічний ключ у Base64 (з документації Monobank)</param>
public static bool Verify(byte[] body, string signatureBase64, string publicKeyBase64)
{
try
{
// Декодуємо підпис та публічний ключ з Base64
var signature = Convert.FromBase64String(signatureBase64);
var publicKeyBytes = Convert.FromBase64String(publicKeyBase64);
// Імпортуємо публічний ключ з X.509 SubjectPublicKeyInfo формату
using var ecdsa = ECDsa.Create();
ecdsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);
// Верифікуємо підпис. Monobank використовує SHA256 digest.
// DSASignatureFormat.Rfc3279DerSequence — стандартний DER-формат підпису.
return ecdsa.VerifyData(
body,
signature,
HashAlgorithmName.SHA256,
DSASignatureFormat.Rfc3279DerSequence);
}
catch (Exception)
{
// Будь-яка помилка при верифікації = підпис невалідний
return false;
}
}
}
MonobankPaymentProviderpublic class MonobankPaymentProvider : IPaymentProvider
{
private readonly MonobankClient _client;
public string ProviderName => "monobank";
public MonobankPaymentProvider(MonobankClient client)
=> _client = client;
public async Task<CreatePaymentResult> CreatePaymentAsync(
PaymentRequest request,
CancellationToken ct = default)
{
var invoiceRequest = new MonobankInvoiceRequest
{
// Конвертуємо гривні в копійки
Amount = (long)(request.Amount * 100),
Ccy = 980, // UAH
MerchantPaymInfo = new MerchantPaymInfo
{
Reference = request.PaymentId.ToString(),
Destination = request.Description
},
RedirectUrl = request.ReturnUrl,
WebHookUrl = request.CallbackUrl
};
var response = await _client.CreateInvoiceAsync(invoiceRequest, ct);
if (response is null)
return new CreatePaymentResult(false, null, null, null,
"Null response from Monobank");
return new CreatePaymentResult(
Success: true,
CheckoutUrl: response.PageUrl,
FormData: response.QrCode, // QR-код у SVG для відображення
ProviderPaymentId: response.InvoiceId,
ErrorMessage: null
);
}
public async Task<PaymentStatusResult> GetPaymentStatusAsync(
string providerTransactionId,
CancellationToken ct = default)
{
var status = await _client.GetInvoiceStatusAsync(providerTransactionId, ct);
if (status is null)
return new PaymentStatusResult(false, PaymentStatus.Failed, null, "Null response");
return new PaymentStatusResult(
Success: true,
Status: MapMonobankStatus(status.Status),
ProviderTransactionId: status.InvoiceId,
ErrorMessage: null
);
}
public async Task<RefundResult> RefundAsync(
string providerTransactionId,
decimal amount,
CancellationToken ct = default)
{
var result = await _client.RefundAsync(providerTransactionId, amount, ct);
return result?.Status == "success"
? new RefundResult(true, result.InvoiceId, null)
: new RefundResult(false, null, "Refund failed");
}
public Task<WebhookResult> ProcessWebhookAsync(
HttpRequest httpRequest,
CancellationToken ct = default)
=> throw new NotSupportedException("Use WebhookEndpoints for Monobank webhook");
private static PaymentStatus MapMonobankStatus(string? status) =>
status switch
{
"success" => PaymentStatus.Settled,
"processing" => PaymentStatus.Pending,
"hold" => PaymentStatus.Authorized,
"failure" => PaymentStatus.Failed,
"reversed" => PaymentStatus.Refunded,
"expired" => PaymentStatus.Failed,
_ => PaymentStatus.Failed
};
}
// Додайте до MapWebhookEndpoints:
app.MapPost("/webhooks/monobank", HandleMonobankWebhook)
.WithTags("Webhooks");
private static async Task<IResult> HandleMonobankWebhook(
HttpRequest request,
IOptions<MonobankOptions> options,
PaymentService paymentService,
ILogger<Program> logger,
CancellationToken ct)
{
// Зчитуємо тіло як байти (необхідно для верифікації підпису!)
using var ms = new MemoryStream();
await request.Body.CopyToAsync(ms, ct);
var bodyBytes = ms.ToArray();
// Отримуємо підпис з заголовка X-Sign
var signature = request.Headers["X-Sign"].ToString();
if (string.IsNullOrEmpty(signature))
{
logger.LogWarning("Monobank webhook: missing X-Sign header");
return Results.BadRequest();
}
// Верифікуємо ECDSA підпис
var isValid = MonobankSignatureVerifier.Verify(
bodyBytes, signature, options.Value.WebhookPublicKey);
if (!isValid)
{
logger.LogWarning("Monobank webhook: invalid ECDSA signature");
return Results.Ok(); // Відповідаємо 200 щоб уникнути retry
}
// Парсимо JSON payload
var payload = JsonSerializer.Deserialize<MonobankWebhookPayload>(bodyBytes);
if (payload is null) return Results.Ok("Parse error");
logger.LogInformation(
"Monobank webhook: invoiceId={InvoiceId}, status={Status}",
payload.InvoiceId, payload.Status);
// Знаходимо payment за invoiceId та оновлюємо статус
await paymentService.UpdatePaymentByProviderIdAsync(
providerTransactionId: payload.InvoiceId,
newStatus: MapStatus(payload.Status),
rawPayload: Encoding.UTF8.GetString(bodyBytes));
return Results.Ok();
}
private static PaymentStatus MapStatus(string? status) =>
status switch
{
"success" => PaymentStatus.Settled,
"failure" => PaymentStatus.Failed,
"reversed" => PaymentStatus.Refunded,
_ => PaymentStatus.Pending
};
public record MonobankWebhookPayload(
[property: JsonPropertyName("invoiceId")] string InvoiceId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("amount")] long Amount,
[property: JsonPropertyName("ccy")] int Ccy,
[property: JsonPropertyName("reference")] string? Reference
);
Monobank Acquiring виділяється серед українських PSP чистим REST API та сучаснішою криптографією (ECDSA замість HMAC-SHA1). Ключові відмінності від LiqPay: сума передається у копійках, автентифікація через Bearer токен, а верифікація webhook потребує імпорту ECDSA публічного ключа. Завдяки IPaymentProvider-абстракції, підключення Monobank не вплинуло на логіку PaymentService — ми тільки додали нову реалізацію.