Що таке MonoPay / plata by mono і як виглядає інтеграція

Якщо дуже коротко: ти на своєму бекенді створюєш рахунок (invoice) через API monobank, отримуєш у відповідь invoiceId та pageUrl, відправляєш користувача на оплату, а результат фіксуєш за подією, яку monobank надсилає на твій webHookUrl. Сам monobank прямо рекомендує вважати webhook основним механізмом отримання статусів, а метод перевірки статусу — використовувати лише як “план Б” при розсинхронізації.

Ключові пункти підключення:

  1. Підключення еквайрингу у бізнес-кабінеті та отримання токена (X-Token).

  2. Створення інвойсу (/api/merchant/invoice/create) з сумою, описом, redirectUrl та webHookUrl.

  3. Оплата на сторінці pageUrl (картка/Apple Pay/Google Pay/оплата в застосунку — залежить від сценарію).

  4. Webhook від monobank про зміну статусу інвойсу.

  5. Верифікація підпису webhook (ECDSA, заголовок X-Sign).

  6. Підтвердження у своїй системі: змінюєш статус замовлення/підписки/рахунку.

  7. Резервна перевірка статусу (/api/merchant/invoice/status) якщо треба звіритися.

Реєстрація та підключення еквайрингу: що потрібно зробити до написання коду

Для підключення еквайрингу потрібно бути зареєстрованим як ФОП і мати відкритий рахунок ФОП у mono.

Далі — авторизація у web-кабінеті та заповнення інформації про бізнес (вид діяльності, посилання на сайт/соцмережі), після чого ти обираєш і підключаєш платіжні інструменти.

Це важливо: якщо еквайринг не підключений, токена для API у тебе просто не буде (він “стає доступним після підключення еквайрингу”).

У документації еквайрингу є чіткі кроки отримання токена:

  • зайти у web-кабінет;

  • перейти на вкладку потрібного методу прийому оплат і натиснути “Налаштувати”;

  • Тепер можна створити токен

Цей токен ти потім передаєш у заголовку X-Token для API-запитів.

Тестове середовище

monobank дає можливість працювати у тестовому середовищі: для цього потрібен тестовий токен, а “карткові” дані можуть бути будь-якими валідними (з валідним номером за алгоритмом Луна) — фінансова авторизація не відбудеться, але інтеграцію ти проганяєш.

Архітектура інтеграції: що зберігати у БД

Щоб інтеграція була контрольована (і щоб потім не розбиратися, чому “гроші прийшли, а замовлення не оплатилось”), я раджу зберігати щонайменше:payments / invoices

  • id (внутрішній)

  • order_id (або інший бізнес-ідентифікатор)

  • invoice_id (з monobank)

  • amount (у мінімальних одиницях, наприклад копійки)

  • ccy (ISO 4217, за замовчуванням 980)

  • status (created/processing/success/expired/failure тощо)

  • created_at, updated_at

  • modified_date (з monobank — критично для правильного упорядкування вебхуків)

  • raw_last_payload (не обов’язково, але корисно для дебагу)

Чому modifiedDate важливий?
monobank попереджає, що вебхуки не гарантуються “один за одним”, і теоретично status=success може прийти раніше за status=processing. Тому “правильна істина” — це подія з більшим modifiedDate.

Також врахуй повтори: бекенд еквайрингу робить до 3 спроб POST вебхуку, доки не отримає 200 OK. Це означає: твій webhook-handler має бути ідемпотентним (повторне отримання того ж стану не повинно ламати систему).

Створення рахунку (invoice/create): ключові параметри та приклад запиту

Ендпоінт

Офіційний метод створення рахунку:

POST https://api.monobank.ua/api/merchant/invoice/create

Заголовки

  • X-Token: <токен з кабінету>

  • опційно: X-Cms / X-Cms-Version — якщо ти робиш модуль для CMS (корисно, якщо інтеграція пакується як плагін).

Тіло запиту: що мінімально потрібно

Мінімально зазвичай використовують:

  • amount — сума у мінімальних одиницях (для гривні — копійки)

  • ccy — 980 за замовчуванням

  • merchantPaymInfo — “людська” інформація про платіж: reference, destination, comment, кошик basketOrder (особливо актуально для e-commerce)

  • redirectUrl — куди повернути клієнта після завершення оплати (успіх або помилка)

  • webHookUrl — куди monobank надсилатиме статус при зміні (крім expired)

  • validity — строк життя рахунку в секундах (за замовчуванням 24 години)

  • paymentTypedebit або hold (якщо потрібен холд з подальшою фіналізацією)

У відповідь ти отримуєш:

  • invoiceId

  • pageUrl (посилання на оплату)

Приклад на PHP 8.x (cURL)


$token = getenv('MONO_X_TOKEN');

$payload = [
    'amount' => 19900,        // 199.00 UAH -> у копійках
    'ccy'    => 980,
    'merchantPaymInfo' => [
        'reference'   => 'ORDER-100045',     // твій унікальний референс
        'destination' => 'Оплата замовлення #100045',
        'comment'     => 'Дякуємо за покупку',
        'basketOrder' => [
            [
                'name'  => 'Підписка PRO, 1 місяць',
                'qty'   => 1,
                'sum'   => 19900,
                'total' => 19900,
                'unit'  => 'шт.',
                'code'  => 'SUB-PRO-1M',
            ],
        ],
    ],
    'redirectUrl' => 'https://example.com/pay/return',
    'webHookUrl'  => 'https://example.com/pay/mono/webhook',
    'validity'    => 3600,
    'paymentType' => 'debit',
];

$ch = curl_init('https://api.monobank.ua/api/merchant/invoice/create');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'X-Token: ' . $token,
    ],
    CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode !== 200) {
    throw new RuntimeException("Monobank create invoice failed: HTTP $httpCode; body=$response");
}

$data = json_decode($response, true);

$invoiceId = $data['invoiceId'];
$pageUrl   = $data['pageUrl'];

// 1) зберігаєш invoiceId у БД разом із order_id
// 2) редіректиш клієнта на $pageUrl
header('Location: ' . $pageUrl, true, 302);
exit;

Семантика полів і сам ендпоінт відповідають документації: сума в мінімальних одиницях, redirectUrl, webHookUrl, а відповідь містить invoiceId і pageUrl.

Повернення користувача: redirectUrl

Після оплати користувача перекине на твій redirectUrl (GET). Тут часто роблять сторінки:

  • “Оплата успішна, ми обробляємо замовлення”

  • “Оплата не пройшла, спробуйте ще раз”

Але принципово важливо: не треба підтверджувати оплату лише на основі redirect-факту.
Redirect — це клієнтський UX, який можна перервати, підмінити, зафаєрволити, або користувач може просто закрити вкладку.

На стороні серверу треба оброблювати:

  • webhook зі статусом (з верифікацією підпису), або

  • резервно: запит статуса інвойсу.

Webhook: прийом подій і верифікація підпису (ECDSA, X-Sign)

Як приходять вебхуки

monobank надсилає POST на webHookUrl при зміні статусу (крім expired), тіло запиту ідентичне відповіді методу “Статус рахунку”, а в заголовках є X-Sign — підпис тіла webhook по стандарту ECDSA.

Також важливі дві особливості:

  • до 3 спроб доставки до 200 OK;
  • події можуть прийти не в хронологічному порядку, орієнтуйся на modifiedDate.

Отримання публічного ключа

У прикладі з документації вказано, що публічний ключ для верифікації потрібно отримати через ендпоінт https://api.monobank.ua/api/merchant/pubkey.

Практичний PHP-приклад верифікації

Нижче — приклад, адаптований під реальний webhook-handler. Логіка:

  1. читаємо raw body;

  2. беремо X-Sign;

  3. верифікуємо через openssl_verify;

  4. якщо ок — парсимо JSON і оновлюємо статус у БД з урахуванням modifiedDate.

function getMonoPubKeyPem(string $token): string
{
    $ch = curl_init('https://api.monobank.ua/api/merchant/pubkey');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ['X-Token: ' . $token],
    ]);
    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($code !== 200) {
        throw new RuntimeException("Cannot fetch monobank pubkey: HTTP $code; body=$resp");
    }

    // У документації приклади працюють з base64-представленням PEM.
    // Тут припустимо, що API повертає base64-рядок, який треба decode.
    return base64_decode(trim($resp));
}

$token = getenv('MONO_X_TOKEN');

$rawBody = file_get_contents('php://input');
$xSign   = $_SERVER['HTTP_X_SIGN'] ?? '';

if ($xSign === '') {
    http_response_code(400);
    echo 'Missing X-Sign';
    exit;
}

$signature = base64_decode($xSign);
$pubKeyPem = getMonoPubKeyPem($token);

$pubKey = openssl_get_publickey($pubKeyPem);
if ($pubKey === false) {
    http_response_code(500);
    echo 'Invalid pubkey';
    exit;
}

// OPENSSL_ALGO_SHA256 — як у прикладах документації
$ok = openssl_verify($rawBody, $signature, $pubKey, OPENSSL_ALGO_SHA256);

if ($ok !== 1) {
    http_response_code(401);
    echo 'Invalid signature';
    exit;
}

$payload = json_decode($rawBody, true);
if (!is_array($payload) || empty($payload['invoiceId'])) {
    http_response_code(400);
    echo 'Bad payload';
    exit;
}

$invoiceId    = $payload['invoiceId'];
$status       = $payload['status'] ?? 'unknown';
$modifiedDate = $payload['modifiedDate'] ?? null;

// Далі — твоя бізнес-логіка:
// 1) знайти запис в БД за invoiceId
// 2) порівняти modifiedDate (не оновлювати старішим payload)
// 3) якщо status=success -> відмітити замовлення як оплачене
// 4) повернути 200 OK, щоб monobank не ретраїв webhook
http_response_code(200);
echo 'OK';

Важливі деталі (вони з документації):

  • X-Sign — ECDSA-підпис тіла webhook;
  • modifiedDate — головний критерій “який статус актуальний”;
  • тіло webhook відповідає структурі “статусу рахунку”, де є invoiceId, status, суми, причини помилки тощо.

Статуси та поля відповіді: що реально використовувати в бізнес-логіці

Метод статусу інвойсу повертає корисний набір полів, серед яких:

  • invoiceId

  • status (наприклад created)

  • failureReason та errCode (коли платіж не пройшов)

  • amount, ccy, finalAmount

  • createdDate, modifiedDate

  • reference, destination

  • paymentInfo (маскована картка, rrn, tranId, тощо — корисно для звірок)

  • cancelList, інші об’єкти

У прикладі з документації прямо видно ці поля у response sample.

Практична рекомендація:
для більшості e-commerce сценаріїв ти використовуєш як мінімум:

  • status

  • invoiceId

  • modifiedDate

  • finalAmount (якщо в тебе можливі часткові списання/комісії/холди)

А failureReason / errCode — щоб показати людині адекватне повідомлення (“Недостатньо коштів”, “Невірний CVV”, тощо) і не перетворювати оплату на “магію, що не працює”.

Перевірка статусу платежу (invoice/status) як “страховка”

Ендпоінт:

  • GET /api/merchant/invoice/status?invoiceId={invoiceId} з заголовком X-Token.

monobank прямо каже: не використовуй цей сервіс як основний механізм після проведення операції; краще вебхуки. Але для кейсів “webhook не прийшов” або “на боці продавця був даунтайм” — це правильний інструмент для відновлення консистентності.

Приклад на PHP

$token = getenv('MONO_X_TOKEN');
$invoiceId = $_GET['invoiceId'];

$url = 'https://api.monobank.ua/api/merchant/invoice/status?invoiceId=' . urlencode($invoiceId);

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-Token: ' . $token],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode !== 200) {
    throw new RuntimeException("Monobank status failed: HTTP $httpCode; body=$response");
}

$data = json_decode($response, true);

// Далі: звіряєш data['status'], data['modifiedDate'], data['finalAmount'] і оновлюєш БД

Готові модулі та інтеграції: коли краще “не писати код з нуля”

Якщо твій сайт уже працює на популярній CMS/конструкторі, інколи найкраще рішення — не винаходити велосипед, а поставити готову інтеграцію.

Офіційна сторінка готових інтеграцій

monobank має централізовану сторінку, де зібрані готові інтеграції та платіжні модулі для різних систем (“натиснувши на лого партнера, можна перейти на інструкцію з налаштування”).

Це варіант “мінімум кастому”: вставив токен у налаштування — і працює.

Приклад: WooCommerce / WordPress

Є офіційний плагін для WordPress/WooCommerce (Monobank WP Payment Plugin), який позиціонується як офіційний модуль для підключення інтернет-еквайрингу.

Приклад: OpenCart

В екосистемі OpenCart теж є модулі під MonoPay (включно з публічними каталогами/форумами модулів).

Коли модуль — кращий вибір

Обирай модуль, якщо:

  • тобі потрібно “швидко запустити оплату” без кастомної логіки;

  • ти не плануєш складні сценарії (холди, токенізація, спліт-платежі, свій білінг);

  • ти хочеш підтримку оновлень “у рамках CMS”.

Коли API-інтеграція потрібна обов’язково

Пиши свою інтеграцію через API, якщо:

  • у тебе кастомний бекенд (Laravel/CI/Symfony/Node/Go тощо) і нетипова логіка оплат;

  • потрібні складні сценарії (наприклад, холд/фіналізація, токенізація, мульти-продукти з власними правилами);

  • ти хочеш повний контроль: власні ретраї, звірки, аудит подій, антифрод-логіку.

 

Практичний чек-лист “зробив і забув”: як довести інтеграцію до production-якості

  1. Токен зберігай тільки в секретах/ENV (не в коді, не в репозиторії).

  2. reference роби унікальним на твоєму боці (наприклад, ORDER-). Це дуже допомагає у звірках.

  3. Webhook-endpoint:

    • повинен швидко відповідати 200 OK;

    • має бути ідемпотентним;

    • обов’язково верифікує X-Sign.

  4. Логіка оновлення статусів:

    • не довіряй порядку приходу подій;

    • завжди порівнюй modifiedDate і не перезаписуй “новий” статус “старим”.

  5. Зроби reconciliation job (крон/черга): раз на N хвилин перевіряй “завислі” інвойси через invoice/status (тільки для тих, у кого давно не було webhook). Це узгоджується з призначенням методу статусу як резервного.

  6. Перед запуском у прод:

    • прогони тестове середовище з тестовим токеном;

    • перевір, що твої URL доступні з інтернету і не блокуються WAF/Cloudflare;

    • перевір, що “success” реально переводить замовлення в “paid” тільки на сервері (вебхук/статус), а не “по редіректу”.

 

Інтеграція MonoPay (plata by mono) добре лягає на сучасну архітектуру “інвойс → платіжна сторінка → вебхук → підтвердження в БД”, але якість рішення визначають три речі:

  • коректне підключення і керування токеном у бізнес-кабінеті;

  • правильна обробка вебхуків (підпис + modifiedDate + ідемпотентність);

  • наявність резервного механізму звірки через invoice/status для рідкісних розсинхронізацій.