API для розробників
REST API для платіжних посилань з QR-кодом НБУ. Створюйте посилання зі своєї CRM, сайту чи бота, а про оплату дізнавайтеся з вебхука.
Початок роботи
- Увійдіть у кабінет → API і створіть ключ. Його покажуть один раз, збережіть його в змінних середовища свого сервера.
- Створіть посилання запитом нижче і віддайте клієнту
urlабоshareText. - Додайте адресу вебхука в тому ж розділі кабінету: МЦР повідомить, коли посилання оплачено.
Базова адреса: https://mcr.procrm.ua/api/v1
curl -X POST https://mcr.procrm.ua/api/v1/links \
-H "Authorization: Bearer $MCR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipient": { "name": "ТОВ «Приклад»", "iban": "UA213223130000026007233566001", "code": "41234569" },
"purpose": "Оплата за замовленням 1042",
"amount": 1250.50,
"validUntil": "2026-11-30",
"externalId": "order-1042"
}'const res = await fetch("https://mcr.procrm.ua/api/v1/links", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.MCR_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
recipientId: "6f1c0a52-2a8e-4c4b-9a39-0b3e7f6a1d20",
purpose: "Оплата за замовленням 1042",
amount: 1250.5,
externalId: "order-1042",
}),
});
const link = await res.json();
if (!res.ok) throw new Error(link.error.message);
// link.url: сторінка оплати з QR-кодом, link.shareText: готовий текст для месенджераКлюч і ліміти
Кожен запит передає ключ у заголовку Authorization: Bearer mcr_…. Ключ дає доступ до всіх посилань і одержувачів кабінету, у якому його створили. До 10 ключів на кабінет; відкликаний ключ перестає працювати одразу.
Викликайте API лише зі свого сервера. API не відповідає на запити з браузера (немає CORS), тому ключ не можна покласти в код сторінки чи мобільного застосунку. Віджет чату на сайті звертається до вашого бекенду, а бекенд до МЦР.
Ліміт: 60 запитів на хвилину для одного ключа. Кожна відповідь містить заголовки X-RateLimit-Limit і X-RateLimit-Remaining. Понад ліміт приходить 429 із заголовком Retry-After (секунди до нової хвилини).
Формат даних
- Запити й відповіді в JSON (UTF-8), заголовок
Content-Type: application/json, тіло до 64 КБ. - Імена полів у camelCase. Невідоме поле в запиті дає помилку
422, тож одруківка не пройде непомітно. - Суми: число з двома знаками після крапки (
1250.5) або рядок ("1250.50"). validUntil: датаYYYY-MM-DD, посилання діє до кінця цього дня за київським часом.- Час (
createdAt,paidAt): ISO 8601 в UTC.
Помилки
Помилка має HTTP-статус і тіло з кодом і поясненням українською. Для 422 поле fields називає кожне неправильне поле.
{
"error": {
"code": "validation_failed",
"message": "Перевірте поля в fields",
"fields": {
"recipient.iban": "Контрольна сума не збігається: перевірте цифри",
"amount": "Сума має бути більшою за нуль"
}
}
}| Статус | code | Коли |
|---|---|---|
| 400 | invalid_json | Тіло не є коректним JSON |
| 401 | unauthorized | Немає ключа, ключ неправильний або відкликаний |
| 404 | not_found | Посилання немає у вашому кабінеті |
| 409 | link_paid, link_cancelled | Дія не підходить до стану посилання |
| 413 | too_large | Тіло більше за 64 КБ |
| 422 | validation_failed | Неправильні поля, деталі в fields |
| 429 | rate_limited | Перевищено ліміт запитів |
| 500 | internal | Збій на боці МЦР, повторіть запит |
| 503 | rates_unavailable | Курс НБУ недоступний, а rate не передано |
Об’єкт «Посилання»
Однаковий у відповідях API і в тілі вебхуків.
| Поле | Тип | Опис |
|---|---|---|
id | uuid | Ідентифікатор посилання |
slug | string | Публічний код з 8 символів. Він же код оплати в призначенні платежу |
url | string | Сторінка оплати з QR-кодом НБУ і кнопками банків |
pdfUrl | string | Рахунок у PDF з QR-кодом |
status | string | active, paid, cancelled або expired (активне, але минув validUntil) |
amount | number | null | Гривні в QR-коді. null: суму вводить платник |
currency | string | Валюта рахунку, UAH за замовчуванням |
currencyAmount, rate, markupPercent | number | null | Для валютних посилань: сума у валюті, курс (грн за 1 одиницю) і націнка у відсотках |
rateSource, rateDate | string | null | Звідки курс: nbu (курс НБУ на rateDate), custom (ваш) |
items | array | Позиції: name, qty, price, total |
recipient | object | name, iban, code (ЄДРПОУ або РНОКПП) |
purpose | string | Призначення платежу разом із кодом оплати |
validUntil | date | null | Останній день оплати |
paidAt | datetime | null | Коли оплачено |
externalId, metadata | Ваші дані з запиту на створення | |
views | integer | Скільки разів платники відкрили сторінку |
shareText | string | Текст для месенджера чи листа: сума, одержувач, призначення, посилання |
createdAt, updatedAt | datetime |
Створити посилання
POST/links
| Поле | Тип | Опис |
|---|---|---|
recipient | object | Одержувач: { name, iban, code }. Або recipientId |
recipientId | uuid | Збережений одержувач з GET /recipients замість recipient |
purpose | string | Обов’язкове. Призначення платежу, до 398 символів: МЦР додає до нього « (код оплати …)» |
amount | number | Сума у валюті currency. Без amount і items суму вводить платник (лише UAH) |
items | array | Замість amount: до 50 позицій { name, qty, price }, сума рахується з них |
currency | string | Код ISO 4217, за замовчуванням UAH |
rate | number | Для валюти: гривень за 1 одиницю, до 4 знаків. Без нього береться курс НБУ на сьогодні |
markupPercent | number | Для валюти: націнка до курсу, від 0 до 100 |
validUntil | date | Останній день оплати. Без нього посилання діє безстроково |
title | string | Ваша назва до 100 символів, платник її не бачить |
externalId | string | Ваш id угоди, замовлення чи чату, до 100 символів, унікальний у кабінеті |
metadata | object | До 20 пар «ключ: значення» (рядки, числа, true/false), повертаються в API і вебхуках |
Реквізити перевіряються так само, як у формі на сайті: контрольна сума IBAN і ЄДРПОУ/РНОКПП, довжина полів, обмеження QR-коду НБУ. Для валюти гривні в QR-коді дорівнюють сумі × курс × (1 + націнка / 100), з округленням до копійки.
Відповідь 201 з посиланням. Якщо посилання з таким externalId вже є, приходить 200 з ним без змін. Тож запит безпечно повторити після таймауту: дубля не буде. Для нового посилання до тієї самої угоди передайте новий externalId, наприклад order-1042-2.
{
"id": "0b8e6f1c-5d0a-4f43-9a8e-2f1d3c4b5a69",
"slug": "k7m2q9xa",
"url": "https://mcr.procrm.ua/p/k7m2q9xa",
"pdfUrl": "https://mcr.procrm.ua/p/k7m2q9xa/pdf",
"status": "active",
"title": "Замовлення 1042",
"amount": 1250.5,
"currency": "UAH",
"currencyAmount": null,
"rate": null,
"rateSource": null,
"rateDate": null,
"markupPercent": null,
"items": [],
"recipient": { "name": "ТОВ «Приклад»", "iban": "UA213223130000026007233566001", "code": "41234569" },
"purpose": "Оплата за замовленням 1042 (код оплати k7m2q9xa)",
"validUntil": "2026-11-30",
"paidAt": null,
"externalId": "order-1042",
"metadata": { "chatId": "tg-55120" },
"views": 0,
"shareText": "До сплати: 1 250,50 грн\nОдержувач: ТОВ «Приклад»\n…",
"createdAt": "2026-10-29T12:00:00.000Z",
"updatedAt": "2026-10-29T12:00:00.000Z"
}{
"recipientId": "6f1c0a52-2a8e-4c4b-9a39-0b3e7f6a1d20",
"purpose": "Оплата за рахунком № 77",
"currency": "EUR",
"markupPercent": 1.5,
"items": [
{ "name": "Ліцензія, 12 місяців", "qty": 3, "price": 120 },
{ "name": "Налаштування", "qty": 1, "price": 200 }
]
}Список посилань
GET/links?status=paid&limit=50
| Поле | Тип | Опис |
|---|---|---|
status | string | active, paid, cancelled або expired |
externalId | string | Посилання з цим externalId |
limit | integer | Від 1 до 100, за замовчуванням 50 |
cursor | string | nextCursor з попередньої відповіді |
Новіші першими. nextCursor дорівнює null на останній сторінці.
{ "data": [ { "id": "0b8e6f1c-…", "status": "paid", … } ], "nextCursor": "0b8e6f1c-5d0a-4f43-9a8e-2f1d3c4b5a69" }Одне посилання
GET/links/{id}
{id}: id або slug. Знайти посилання за своїм id можна через GET /links?externalId=order-1042.
Позначити оплаченим
POST/links/{id}/paid
Коли гроші прийшли іншим шляхом: готівка, картка, інший рахунок. Тіло необов’язкове: { "paidAt": "2026-10-29T14:05:00+02:00" }, без нього береться поточний час. Сторінка оплати перестає показувати QR-код, вебхук отримує link.paid.
Уже оплачене посилання повертається без змін (200), скасоване дає 409 link_cancelled.
Скасувати
POST/links/{id}/cancel
Без тіла. Сторінка оплати перестає показувати QR-код, вебхук отримує link.cancelled. Уже скасоване повертається без змін, оплачене дає 409 link_paid.
Одержувачі
GET/recipients
Одержувачі, збережені в кабінеті. Їхній id передавайте як recipientId, щоб не надсилати реквізити щоразу.
{ "data": [ { "id": "6f1c0a52-2a8e-4c4b-9a39-0b3e7f6a1d20", "name": "ТОВ «Приклад»", "iban": "UA213223130000026007233566001", "code": "41234569", "createdAt": "2026-10-20T09:00:00.000Z" } ] }Вебхуки
Адресу додають у кабінеті → API: лише https, до 5 адрес, для кожної свій секрет підпису. Секрет показують один раз; якщо його втрачено, створіть новий там само.
| Поле | Тип | Опис |
|---|---|---|
link.paid | подія | Посилання оплачено: за банківською випискою (ПриватБанк, monobank), вручну в кабінеті або через API |
link.cancelled | подія | Посилання скасовано в кабінеті або через API |
link.reopened | подія | Посилання знову активне: власник у кабінеті зняв позначку оплати або відновив скасоване. Платник знову бачить QR-код, paidAt дорівнює null |
ping | подія | Кнопка «Тестовий запит» у кабінеті |
МЦР надсилає POST з JSON і такими заголовками:
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: MCR-Webhooks/1.0
X-MCR-Event: link.paid
X-MCR-Delivery: 9d2f4c1e-7a3b-4e8f-b1c2-5a6d7e8f9a0b
X-MCR-Signature: t=1793275500,v1=5f0c2b…
{
"id": "3c1e2d4f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"type": "link.paid",
"createdAt": "2026-10-29T12:05:00.000Z",
"data": { "link": { "id": "0b8e6f1c-…", "status": "paid", "paidAt": "2026-10-29T12:04:31.000Z", "externalId": "order-1042", … } }
}- Відповідайте кодом 2xx протягом 8 секунд, довгу обробку робіть після відповіді. Переспрямування (3xx) МЦР не виконує.
- Без 2xx МЦР повторює запит до 6 разів, приблизно через 1 хв, 5 хв, 30 хв, 2 год, 6 год і 24 год. Після цього доставку позначено «Не доставлено», її можна повторити в кабінеті вручну.
- Повтор надсилає те саме тіло з тим самим
id. Зберігайте обробленіidі пропускайте повтори. - Порядок подій не гарантовано: орієнтуйтеся на
data.link.statusіdata.link.updatedAt. - Адреси у внутрішніх мережах (localhost, 10.x, 192.168.x тощо) МЦР не викликає.
Перевірка підпису
Заголовок X-MCR-Signature має вигляд t=<час>,v1=<підпис>. t: Unix-час надсилання в секундах. v1: HMAC-SHA256 у hex від рядка t + "." + тіло, ключ: секрет адреси. Беріть тіло точно таким, як воно прийшло, до розбору JSON. Відхиляйте запити, старші за 5 хвилин: так перехоплений запит не вдасться відправити вдруге.
import { createHmac, timingSafeEqual } from "node:crypto";
/** rawBody: тіло запиту як рядок або Buffer, до JSON.parse. */
export function verifyMcrSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=", 2)));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
const got = Buffer.from(parts.v1 ?? "");
return got.length === expected.length && timingSafeEqual(got, expected);
}function verify_mcr_signature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
$parts[$key] = $value;
}
$t = (int) ($parts['t'] ?? 0);
if ($t === 0 || abs(time() - $t) > $tolerance) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}
$raw = file_get_contents('php://input');
if (!verify_mcr_signature($raw, $_SERVER['HTTP_X_MCR_SIGNATURE'] ?? '', getenv('MCR_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($raw, true);
http_response_code(200);import hashlib, hmac, time
def verify_mcr_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts.get("t", ""))
except ValueError:
return False
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))OpenAPI
Опис API у форматі OpenAPI 3.1: https://mcr.procrm.ua/api/v1/openapi.json. Його можна імпортувати в Postman чи Insomnia або згенерувати з нього клієнт для своєї мови.

