# Jak podłączyć e-acquiring Monobank bez programisty dzięki vibe codingowi

> Przewodnik krok po kroku dotyczący integracji płatności online Monobank ze stroną internetową: compliance dla jednoosobowych działalności gospodarczych, tworzenie terminala, weryfikacja webhooków ECDSA, niezawodne UX oraz testy bezpieczeństwa.

Vibe coding fundamentalnie zmienił sposób tworzenia cyfrowych produktów. Dziś, aby dodać na stronę obsługę płatności przez Apple Pay, Google Pay lub karty bankowe, nie trzeba już zatrudniać backend developera ani samodzielnie zgłębiać kryptograficznych protokołów. Wystarczy precyzyjnie sformułować logikę biznesową dla swojego agenta AI (Codex, Antigravity, Cursor lub Claude Code) i dostarczyć mu odpowiedni kontekst z dokumentacji technicznej.

W tym poradniku omówiono pełny cykl życia integracji e-commerce z Monobank Acquiring: od przejścia compliance banku i konfiguracji konta przedsiębiorcy, przez generowanie bezpiecznej bramki płatniczej z weryfikacją podpisów ECDSA w webhookach, obsługę race condition w interfejsie, aż po zestaw obowiązkowych testów automatycznych.

> [!TIP]
> **Wersja wideo poradnika:** Jeśli preferujesz format wizualny, obejrzyj [szczegółowe praktyczne wideo na YouTube →](https://youtu.be/GMh_fOCiQ4E), gdzie cały proces został pokazany na żywo, od pierwszego promptu do rzeczywistego pobrania środków.

> [!NOTE]
> **Pakiet skillsów agentowych:** Aby zapewnić maksymalną precyzję generowania kodu, pobierz oficjalny pakiet wiedzy dla swojego asystenta AI:  
> [Pobierz pełne archiwum monobank-acquiring.zip (38 KB) →](/downloads/monobank-acquiring.zip)

---

## 1. Architektura Hosted Checkout i cykl życia płatności

Monobank Acquiring działa w modelu **Hosted Checkout** (płatność na zabezpieczonej stronie banku). Oznacza to, że Twoja strona nie wymaga skomplikowanej i kosztownej certyfikacji bezpieczeństwa PCI DSS, ponieważ dane kart użytkowników są wprowadzane bezpośrednio na zabezpieczonym domenie banku.

Oficjalna dokumentacja Monobank dla narzędzi AI jest dostępna pod adresem [monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts →](https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts). Zapisz ten link jako kanoniczne źródło wymagań technicznych banku.

### 1.1. Podstawowy flow płatności

```
[ Klient na stronie ]
       │
       ├─ 1. Kliknięcie „Zapłać”
       ▼
[ Twój Serwer ] ──────────────► [ API Monobank: /invoice/create ]
       ▲                              │
       │  odbiera pageUrl, invoiceId  │
       └──────────────────────────────┘
       │
       ├─ 2. Przekierowanie klienta na pageUrl (Hosted Checkout)
       ▼
[ Strona płatności Monobank ] ───► Płatność (Apple Pay / Google Pay / Karta)
       │
       ├─ 3. Asynchroniczne POST webhook z podpisem x-sign
       ▼
[ Twój Endpoint Webhooka ] ─────────► Weryfikacja ECDSA SHA-256 → Status "success"
       │
       ├─ 4. Klient wraca na /payment-result
       ▼
[ Strona sukcesu ] ────────────► Wyświetlenie dostępu / paragonu
```

- **1. Utworzenie rachunku (`invoice/create`):** Klient na Twojej stronie wybiera produkt lub plan taryfowy. Handler serwera wysyła żądanie `POST` do API Monobank z kwotą, opisem płatności i adresem zwrotnym. Bank zwraca unikalny `invoiceId` oraz link `pageUrl`.
- **2. Przekierowanie do strony płatności:** Klient zostaje przekierowany na `pageUrl`, gdzie opłaca zamówienie za pomocą Apple Pay, Google Pay, aplikacji Monobank lub wpisując dane karty bankowej.
- **3. Otrzymanie Webhooka:** Po zakończeniu płatności Monobank automatycznie wysyła żądanie `POST` na wcześniej skonfigurowany `webHookUrl` ze statusem transakcji i kryptograficznym podpisem cyfrowym.
- **4. Utrwalenie statusu (Terminal States):** Twój serwer musi reagować na dwa końcowe statusy: `success` (pomyślna płatność – udostępniamy dostęp lub wysyłamy zamówienie) oraz `failure` (błąd lub odrzucenie płatności przez bank).

> [!IMPORTANT]
> Status `expired` (wygasł czas ważności linku) **nie generuje webhooka**. Jeśli klient zamknął stronę płatności i nie zapłacił, stan zamówienia należy sprawdzać poprzez rezerwowe odpytywanie API (polling).

---

## 2. Wymagania prawne i checklista compliance strony dla jednoosobowej działalności gospodarczej (JDG/FOP)

Nawet jeśli kod płatności jest napisany bezbłędnie, dział monitoringu finansowego i bezpieczeństwa Monobank nie aktywuje produkcyjnego przyjmowania płatności, jeśli strona nie spełnia wymogów ukraińskiego prawa oraz zasad systemów płatniczych Visa/Mastercard.

### 2.1. Warunki bankowe
- **Otwarte konto JDG (FOP) lub spółki w Monobank:** Acquiring jest podłączany wyłącznie do kont firmowych. Przyjmowanie płatności komercyjnych na osobistą czarną lub białą kartę osoby fizycznej jest zabronione przez prawo.
- **Odpowiednie kody PKD (KVED):** W danych rejestracyjnych JDG muszą być wskazane kody działalności dla handlu internetowego lub usług (np. `47.91` – Handel detaliczny przez Internet, `62.01`/`62.02` – Programowanie komputerowe i konsultacje, `85.59` – Inne rodzaje edukacji).

### 2.2. Obowiązkowa checklista stron przed złożeniem wniosku o moderację

Zanim złożysz wniosek o aktywację terminala internetowego, upewnij się, że na Twojej stronie znajdują się następujące sekcje (zazwyczaj umieszczane w stopce):

- [ ] **Publiczna oferta (Umowa publiczna):** Opis przedmiotu umowy, moment zawarcia kontraktu oraz praw i obowiązków stron.
- [ ] **Polityka prywatności (Privacy Policy):** Jasne sformułowanie, jakie dane klientów są zbierane i jak są chronione zgodnie z ustawą Ukrainy „O ochronie danych osobowych”.
- [ ] **Warunki zwrotu pieniędzy i dostawy:** Procedura zwrotu towaru lub środków w ciągu 14 dni zgodnie z ustawą Ukrainy „O ochronie praw konsumentów”, a dla produktów cyfrowych/subskrypcji – zasady rezygnacji z usługi.
- [ ] **Pełne dane firmy w stopce strony:** Nazwa JDG lub Spółki z o.o., NIP / KRS (EDRPOU), adres siedziby, kontaktowy numer telefonu oraz służbowy e-mail supportu.
- [ ] **Przejrzyste ceny i opisy:** Każdy przycisk płatności musi mieć stałą kwotę w hrywnach (UAH) oraz czytelny opis tego, za co dokładnie płaci klient.

> [!WARNING]
> Jeśli na stronie brakuje oferty, danych JDG lub zamiast cen widnieją placeholdery typu „do ustalenia”, dział bezpieczeństwa Monobank odrzuci rejestrację terminala na etapie weryfikacji.

---

## 3. Tworzenie terminala webowego w panelu Monobank Business

Aby integrować się z API płatności, potrzebujesz osobistego klucza dostępu — `X-Token`. Jest on generowany bezpłatnie wewnątrz panelu klienta biznesowego.

### 3.1. Algorytm krok po kroku otwierania kasy

1. **Autoryzacja:** Przejdź na [web.monobank.ua →](https://web.monobank.ua/) i zaloguj się za pomocą kodu QR przez aplikację Monobank na smartfonie.
2. **Przejście do kasy:** W lewym menu nawigacyjnym wybierz sekcję **«Kasa»**.
3. **Dodanie narzędzia:** Kliknij przycisk **«+ Dodaj narzędzie»** i wybierz opcję **«Płatności na stronie (własna implementacja)»**.
4. **Rejestracja terminala:** Wpisz nazwę projektu (np. `Moja strona` lub `Główny terminal`) i potwierdź operację przyciskiem **«Podłącz»**.
5. **Generowanie tokena API:** Otwórz utworzony terminal, przejdź do zakładki **«Integracje / Klucze API»**, kliknij **«Utwórz token»** i skopiuj wygenerowany klucz.

> 📍 **Nawigacja w panelu:** `web.monobank.ua` → `Kasa` → `+ Dodaj narzędzie` → `Płatności na stronie (własna implementacja)`

![Tworzenie narzędzia płatniczego w panelu Monobank Kasa](/api/guides-media/automation/monobank-acquiring-vibecoding/images/01-monobank-kasa-add-terminal.webp)

> 📍 **Pozyskanie klucza API:** `Kasa` → `Twój terminal` → `Integracja` → `Utwórz X-Token`

![Okno modalne tworzenia i kopiowania tokenu dostępu X-Token](/api/guides-media/automation/monobank-acquiring-vibecoding/images/02-monobank-create-token.webp)

### 3.2. Żelazne zasady bezpieczeństwa tokenów

- **Żadnych hardkodów w kodzie klienckim:** `X-Token` daje bezpośredni dostęp do zarządzania Twoimi finansami i zwrotami. Jego przechowywanie w jawnym JavaScript/HTML lub publikacja w publicznych repozytoriach GitHub jest surowo zabroniona.
- **Wykorzystanie zmiennych środowiskowych:** Przechowuj token wyłącznie w pliku `.env` na serwerze pod nazwą `MONOBANK_TOKEN` lub w sekcji Secrets Twojej platformy hostingowej (Vercel, Render, Railway, Replit, Lovable).
- **Token testowy do developmentu:** Do początkowych eksperymentów bank udostępnia osobny token testowy na stronie [api.monobank.ua →](https://api.monobank.ua/), który pozwala symulować transakcje bez pobierania realnych środków.

---

## 4. Oficjalne prompty AI Monobank dla vibe coderów

Zespół Monobank przygotował zestaw oficjalnych promptów systemowych dla agentów AI. Ich główną zaletą jest dokładna zgodność z aktualnymi endpointami banku.

![Strona oficjalnej dokumentacji Monobank dla narzędzi AI](/api/guides-media/automation/monobank-acquiring-vibecoding/images/03-monobank-ai-prompts-page.webp)

> [!WARNING]
> **Główna pułapka dla początkujących – kwota w kopiejkach:** API Monobank przyjmuje wszystkie kwoty wyłącznie w minimalnych jednostkach waluty (kopiejkach). $100\text{ UAH} = 10\,000\text{ kopiejek}$. Jeśli przekażesz `amount: 100`, klient zapłaci tylko 1 hrywnę.

### 4.1. Podstawowy prompt do tworzenia płatności

Skopiuj ten prompt i wyślij go na czat swojego asystenta AI:

```markdown
Chcę dodać na swoją stronę internetową możliwość przyjmowania płatności przez Monobank.

ZADANIE:
Napisz kompletny, gotowy do użycia kod służący do tworzenia płatności i przekierowywania użytkownika do strony płatności.

CO MA SIĘ STAĆ:
1. Na mojej stronie znajduje się przycisk „Przejdź do płatności”.
2. Użytkownik klika w przycisk → następuje utworzenie płatności w systemie Monobank.
3. Użytkownik zostaje przekierowany na stronę płatności Monobank.
4. Po dokonaniu płatności użytkownik wraca na moją stronę (https://mysite.com/payment-result).
5. Sprawdzam status płatności i wyświetlam wynik.

DANE TESTOWE:
- Kwota: 100 hrywien (10000 kopiejek)
- Opis płatności: „Płatność za zamówienie”
- Adres zwrotny dla użytkownika: https://mysite.com/payment-result

MÓJ TOKEN MONOBANK:
Zmienna środowiskowa MONOBANK_TOKEN z pliku .env

DOKUMENTACJA:
- Tworzenie płatności: https://monobank.ua/api-docs/acquiring/methods/ia/post--api--merchant--invoice--create
- Sprawdzanie statusu: https://monobank.ua/api-docs/acquiring/methods/ia/get--api--merchant--invoice--status

Napisz cały kod obsługujący ten przepływ pracy.
```

### 4.2. Prompt do konfiguracji handlerów Webhook

Bez webhooka serwer nie dowie się o fakcie pomyślnej płatności, jeśli klient zamknie przeglądarkę tuż po pobraniu środków:

```markdown
Potrzebuję, aby mój serwer automatycznie wykrywał, kiedy użytkownik opłacił zamówienie.

ZADANIE:
Skonfiguruj automatyczne pobieranie informacji o płatnościach z Monobank (webhook).

CO POWINNO SIĘ STAĆ:
1. Użytkownik dokonuje płatności na stronie Monobank.
2. Monobank automatycznie wysyła żądanie POST na mój serwer.
3. Mój serwer otrzymuje dane: invoiceId, status (success/failure), kwotę.
4. Serwer weryfikuje podpis kryptograficzny (ECDSA SHA-256 za pomocą nagłówka x-sign).
5. Aktualizuje status zamówienia w bazie danych i loguje wynik.

WAŻNE:
- Bezwzględnie zwaliduj nagłówek x-sign przy użyciu klucza publicznego banku.
- Przetwarzaj surową treść żądania (raw body Buffer / raw string), a nie sparsowany JSON.
- Obsługuj statusy: success, failure, processing, hold, expired.
- Zapewnij idempotentność: jeśli ten sam webhook zostanie otrzymany dwukrotnie, nie duplikuj wydania towaru.

Napisz cały kod dla niezawodnego procesora webhooków.
```

---

## 5. Pakiet skillsów monobank-acquiring: wzmacnianie agenta

Jeśli ograniczysz się tylko do krótkiego promptu, asystent AI napisze podstawowy kod (około 6.8/10): przycisk zadziała, ale kod nie będzie zawierał weryfikacji podpisów kryptograficznych, ochrony przed podmianą ceny ani obsługi rzadkich błędów sieciowych.

Aby uzyskać poziom produkcyjny (9.8–10 punktów), do katalogu głównego projektu dodaje się wyspecjalizowany pakiet skillsów **`monobank-acquiring`**.

![Audyt integracji płatniczej: porównanie bezpieczeństwa przed i po użyciu skillu](/api/guides-media/automation/monobank-acquiring-vibecoding/images/05-codex-skills-audit.webp)

```bash
# Розпакування пакета скілів у проєкт
curl -L -o monobank-acquiring.zip https://gotburnout.io/downloads/monobank-acquiring.zip
unzip monobank-acquiring.zip -d monobank-acquiring/
```

### 5.1. Anatomia i struktura pakietu skillsów

| Plik skillu | Co zawiera i za jakie zadania odpowiada |
| :--- | :--- |
| **`SKILL.md`** | **Centralny manifest:** podstawowy flow, autoryzacja `X-Token`, typy danych oraz obsługa błędów `400`, `403`, `429`, `500`. |
| **`quickstart.md`** | **Szybki start:** krok po kroku tutorial tworzenia faktury i fallback-pollingu z gotowymi komendami `curl`. |
| **`invoice.md`** | **Cykl życia rachunków:** endpointy tworzenia faktury, sprawdzania statusu, anulowania i unieważniania linków. |
| **`webhook.md`** | **Zabezpieczenia kryptograficzne:** matematycznie dokładna weryfikacja podpisu ECDSA SHA-256 z nagłówka `x-sign`. |
| **`payment.md`** | **Płatności bezpośrednie:** pobieranie środków zapisanym tokenem karty, transakcje synchroniczne oraz weryfikacja 3DS. |
| **`wallet.md`** | **Tokenizacja kart (Wallet):** bezpieczne przechowywanie środka płatniczego klienta w skarbcu banku do płatności jednym kliknięciem. |
| **`fiscal.md`** | **Paragony i pRRО:** opis struktury koszyka `basketOrder`, kalkulacja podatków, rabatów oraz eksport paragonów do PDF. |
| **`statement.md`** | **Wyciągi i analityka:** pobieranie rejestru udanych operacji za wybrany okres z kalkulacją prowizji bankowych. |
| **`merchant.md`** | **Dane merchant'a:** pobieranie klucza publicznego banku, zarządzanie submerchantami i kasjerami. |
| **`examples/`** | **Gotowe serwery:** działające przykłady serwerów w 6 popularnych językach (Node.js, Python, Go, PHP, C#, Java). |

---

## 6. Realizacja praktyczna: dynamiczny flow płatności

Każdy projekt ma swoją strukturę: ktoś sprzedaje cyfrowe konsultacje ze stałymi stawkami, ktoś inny prowadzi sklep internetowy z dynamicznym koszykiem, a jeszcze inny ma prosty landing page z jednym przyciskiem dotacji lub subskrypcji.

Głównym błędem początkujących jest sztywne „zaszywanie” kwoty (np. 1000 UAH) bezpośrednio w kodzie klienckim przycisku lub w body żądania POST z przeglądarki. To podejście tworzy krytyczną lukę bezpieczeństwa.

### 6.1. Zasada bezpieczeństwa: dynamiczna cena zamiast hardcode'u

- **Nigdy nie ufaj kwocie z przeglądarki:** Jeśli JavaScript po stronie klienta wysyła na serwer `{ price: 1000 }`, atakujący przez Inspektor Kodu (DevTools) lub Postman może podmienić tę wartość na `{ price: 1 }` i kupić dostęp za 1 hrywnę.
- **Serwer jest jedynym źródłem prawdy (Single Source of Truth):** Frontend przekazuje na backend jedynie identyfikator produktu (`productId`), wybrany plan (`planId: "pro"`) lub tablicę identyfikatorów koszyka (`items: [{ id: "book_1", qty: 2 }]`).
- **Automatyczna konwersja na kopiejki:** Serwer pobiera aktualną cenę z pliku konfiguracyjnego lub bazy danych i sam mnoży ją przez 100:
  $$\text{amount} = \text{Math.round}(\text{realPrice} \times 100)$$
  Jeśli zmienisz cenę w bazie lub ogłosisz rabat, rachunek Monobank zostanie wygenerowany z nową poprawną kwotą bez ingerencji w bramkę płatniczą.

### 6.2. Uniwersalny prompt AI do adaptacji pod dowolny projekt

Skopiuj ten prompt i wyślij go na czat swojego agenta AI (Codex, Antigravity, Cursor lub Claude Code). Agent sam przeskanuje pliki Twojej strony, znajdzie istniejące przyciski i ceny oraz zbuduje solidną integrację:

```markdown
Do katalogu głównego naszego projektu dodano oficjalny pakiet wiedzy Monobank Acquiring w folderze /monobank-acquiring.

ZADANIE:
1. Przeanalizuj architekturę naszego projektu: zlokalizuj, gdzie opisane są produkty, usługi, cenniki lub przyciski składania zamówienia/przechodzenia do płatności.
2. Utwórz lub zintegruj bezpieczny serwerowy endpoint tworzenia płatności Monobank:
   - Klient wysyła TYLKO identyfikator produktu/cennika (np. tariffId lub productId), ale NIE kwotę.
   - Serwer pobiera aktualną, rzeczywistą cenę z naszej konfiguracji lub bazy danych i konwertuje ją na grosze (cena * 100).
   - Token jest pobierany bezpiecznie ze zmiennej środowiskowej process.env.MONOBANK_TOKEN.
   - Wykonywane jest żądanie POST do https://api.monobank.ua/api/merchant/invoice/create.
   - Klientowi zwracany jest link pageUrl służący do przejścia do strony checkout banku.
3. Zaktualizuj istniejące przyciski płatności na frontendzie:
   - Dodaj stan ładowania (wskaźnik Loader oraz blokadę przycisku przed wielokrotnym kliknięciem).
   - W przypadku pomyślnej odpowiedzi wykonaj płynne przekierowanie użytkownika na otrzymany link Monobank.
   - Dodaj obsługę błędów z intuicyjnymi powiadomieniami dla użytkownika.
4. Utwórz stronę wyniku płatności (/payment-result), która sprawdza status zamówienia i informuje użytkownika o sukcesie.
5. Napisz testy jednostkowe sprawdzające dynamiczne obliczanie kwot oraz tworzenie faktury.

Wszystkie wymagania techniczne, struktury żądań oraz kody walut czerp z plików w folderze /monobank-acquiring (szczególnie SKILL.md oraz invoice.md).
```

![Praca agenta AI w IDE z analizą struktury i tworzeniem dynamicznego endpointu](/api/guides-media/automation/monobank-acquiring-vibecoding/images/04-codex-initial-prompt.webp)

### 6.3. Szablon architektury backendu do tworzenia faktury

Oto sprawdzone realizacje serwerowe dla dwóch najpopularniejszych stacków webowych:

:::tabs
=== Next.js App Router
```typescript
// app/api/checkout/create-invoice/route.ts
import { NextResponse } from "next/server";

const PRODUCTS_CATALOG: Record<string, { title: string; priceUah: number }> = {
  plan_starter: { title: "Тариф Starter", priceUah: 490 },
  plan_pro:     { title: "Тариф Pro",     priceUah: 990 },
  plan_vip:     { title: "Тариф VIP",     priceUah: 2490 },
};

export async function POST(req: Request) {
  try {
    const { productId } = await req.json();

    // 1. Валідація: ціна формується виключно на бекенді
    const product = PRODUCTS_CATALOG[productId];
    if (!product) {
      return NextResponse.json({ error: "Обраний товар або тариф не знайдено" }, { status: 400 });
    }

    const amountInKopecks = Math.round(product.priceUah * 100);
    const orderReference = `order_${productId}_${Date.now()}`;
    const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://mysite.com";

    // 2. Запит до Monobank API
    const response = await fetch("https://api.monobank.ua/api/merchant/invoice/create", {
      method: "POST",
      headers: {
        "X-Token": process.env.MONOBANK_TOKEN!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        amount: amountInKopecks,
        ccy: 980, // Гривня (ISO 4217)
        merchantPaymInfo: {
          reference: orderReference,
          destination: `Оплата: ${product.title}`,
          comment: `Замовлення ${orderReference}`,
        },
        redirectUrl: `${siteUrl}/payment-result?ref=${orderReference}`,
        webHookUrl: `${siteUrl}/api/payment/webhook`,
        validity: 3600, // Рахунок діє 1 годину
      }),
    });

    const data = await response.json();
    if (!response.ok) {
      return NextResponse.json({ error: data.errText || "Помилка створення рахунку в банку" }, { status: response.status });
    }

    return NextResponse.json({ checkoutUrl: data.pageUrl, invoiceId: data.invoiceId });
  } catch (error) {
    return NextResponse.json({ error: "Внутрішня помилка ініціалізації платежу" }, { status: 500 });
  }
}
```
=== Express / Node.js
```typescript
// server.ts
import express from "express";

const app = express();
app.use(express.json());

const PRODUCTS_CATALOG: Record<string, { title: string; priceUah: number }> = {
  plan_starter: { title: "Тариф Starter", priceUah: 490 },
  plan_pro:     { title: "Тариф Pro",     priceUah: 990 },
  plan_vip:     { title: "Тариф VIP",     priceUah: 2490 },
};

app.post("/api/checkout/create-invoice", async (req, res) => {
  try {
    const { productId } = req.body;

    const product = PRODUCTS_CATALOG[productId];
    if (!product) {
      return res.status(400).json({ error: "Обраний товар або тариф не знайдено" });
    }

    const amountInKopecks = Math.round(product.priceUah * 100);
    const orderReference = `order_${productId}_${Date.now()}`;
    const siteUrl = process.env.SITE_URL || "https://mysite.com";

    const response = await fetch("https://api.monobank.ua/api/merchant/invoice/create", {
      method: "POST",
      headers: {
        "X-Token": process.env.MONOBANK_TOKEN!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        amount: amountInKopecks,
        ccy: 980,
        merchantPaymInfo: {
          reference: orderReference,
          destination: `Оплата: ${product.title}`,
          comment: `Замовлення ${orderReference}`,
        },
        redirectUrl: `${siteUrl}/payment-result?ref=${orderReference}`,
        webHookUrl: `${siteUrl}/api/payment/webhook`,
        validity: 3600,
      }),
    });

    const data = await response.json();
    if (!response.ok) {
      return res.status(response.status).json({ error: data.errText || "Помилка банку" });
    }

    res.json({ checkoutUrl: data.pageUrl, invoiceId: data.invoiceId });
  } catch (error) {
    res.status(500).json({ error: "Внутрішня помилка ініціалізації платежу" });
  }
});
```
:::

---

## 7. Bezpieczna obróbka webhooków i podpis kryptograficzny ECDSA

Najważniejszą częścią każdej integracji finansowej jest weryfikacja powiadomień o płatności. Atakujący może wysłać spreparowane żądanie HTTP na Twój adres `/api/payment/webhook` z fałszywą wiadomością o sukcesie.

Aby temu zapobiec, Monobank podpisuje każdy webhook za pomocą algorytmu asymetrycznego **ECDSA (krzywa secp256r1 / SHA-256)** i przekazuje sygnaturę w nagłówku HTTP `x-sign`.

### 7.1. Dlaczego `JSON.stringify` łamie weryfikację podpisu

> [!CAUTION]
> **Krytyczna pułapka `rawBody`:** Do weryfikacji podpisu ECDSA potrzebny jest **surowo oryginalny strumień bajtów**, który wysłał serwer Monobank. Jeśli spróbujesz sparsować JSON i ponownie wywołać `JSON.stringify(req.body)`, kolejność kluczy, spacje lub znaki nowej linii mogą się zmienić. To doprowadzi do innego hasha SHA-256, a weryfikacja podpisu **gwarantowanie zwróci błąd**!

### 7.2. Implementacja weryfikacji webhooka

Poniżej znajduje się poprawna implementacja pobierania surowego body i weryfikacji dla Next.js App Router oraz Express:

:::tabs
=== Next.js App Router
```typescript
// app/api/payment/webhook/route.ts
import { NextResponse } from "next/server";
import crypto from "crypto";

let cachedPubKey: string | null = null;

async function getMonobankPubKey(token: string): Promise<string> {
  if (cachedPubKey) return cachedPubKey;

  const res = await fetch("https://api.monobank.ua/api/merchant/pubkey", {
    headers: { "X-Token": token },
    next: { revalidate: 86400 }, // Кешуємо публічний ключ на 24 години
  });
  const data = await res.json();
  cachedPubKey = `-----BEGIN PUBLIC KEY-----\n${data.key}\n-----END PUBLIC KEY-----`;
  return cachedPubKey;
}

export async function POST(req: Request) {
  const signature = req.headers.get("x-sign");
  if (!signature) {
    return new NextResponse("Missing x-sign header", { status: 400 });
  }

  // 1. Отримуємо сире незмінене тіло запиту у вигляді тексту
  const rawBody = await req.text();

  try {
    const pubKey = await getMonobankPubKey(process.env.MONOBANK_TOKEN!);

    // 2. Верифікуємо підпис ECDSA SHA-256
    const verifier = crypto.createVerify("SHA256");
    verifier.update(rawBody);
    const isValid = verifier.verify(pubKey, Buffer.from(signature, "base64"));

    if (!isValid) {
      console.error("Вебхук відхилено: недійсний підпис x-sign");
      return new NextResponse("Invalid signature", { status: 400 });
    }

    // 3. Парсимо дані тільки після успішної перевірки криптографії
    const payload = JSON.parse(rawBody);
    const { invoiceId, status, amount, reference } = payload;

    if (status === "success") {
      // Тут активуємо замовлення в базі даних з перевіркою на дублікати (ідемпотентність)
      console.log(`Замовлення ${reference} (${invoiceId}) оплачено на суму ${amount / 100} грн`);
    }

    return new NextResponse("OK", { status: 200 });
  } catch (error) {
    console.error("Помилка обробки вебхука:", error);
    return new NextResponse("Internal verification error", { status: 500 });
  }
}
```
=== Express / Node.js
```typescript
// webhook.ts
import express from "express";
import crypto from "crypto";

const app = express();

// Зберігаємо сирі байти для верифікації криптографічного підпису
app.use(express.json({
  verify: (req: any, _res, buf) => {
    req.rawBody = buf.toString("utf-8");
  }
}));

let cachedPubKey: string | null = null;

async function getMonobankPubKey(token: string): Promise<string> {
  if (cachedPubKey) return cachedPubKey;
  const res = await fetch("https://api.monobank.ua/api/merchant/pubkey", {
    headers: { "X-Token": token },
  });
  const data = await res.json();
  cachedPubKey = `-----BEGIN PUBLIC KEY-----\n${data.key}\n-----END PUBLIC KEY-----`;
  return cachedPubKey;
}

app.post("/api/payment/webhook", async (req: any, res) => {
  const signature = req.headers["x-sign"] as string;
  if (!signature) {
    return res.status(400).send("Missing x-sign header");
  }

  try {
    const pubKey = await getMonobankPubKey(process.env.MONOBANK_TOKEN!);
    const rawBody = req.rawBody;

    const verifier = crypto.createVerify("SHA256");
    verifier.update(rawBody);
    const isValid = verifier.verify(pubKey, Buffer.from(signature, "base64"));

    if (!isValid) {
      return res.status(400).send("Invalid signature");
    }

    const { invoiceId, status, amount, reference } = req.body;
    if (status === "success") {
      console.log(`Успішна оплата ${reference} (${invoiceId}) на суму ${amount / 100} грн`);
    }

    res.sendStatus(200);
  } catch (err) {
    res.status(500).send("Internal verification error");
  }
});
```
:::

---

## 8. Testowanie lokalne webhooków (Localhost & Cloudflare Tunnels)

Częsty problem vibe coderów: przy uruchomieniu serwera na `http://localhost:3000` bank nie może dostarczyć webhooka, ponieważ lokalna maszyna nie posiada publicznego adresu IP w internecie.

Monobank wysyła webhooki wyłącznie na publiczne adresy z ważnym protokołem `HTTPS`. Aby przetestować pełny cykl na swoim komputerze, przepchnij bezpieczny tunel.

### 8.1. Szybkie uruchomienie tunelu (bez rejestracji i za darmo)

Skorzystaj z Cloudflare Tunnel (`cloudflared`) lub `untun`:

:::tabs
=== Cloudflare (npx untun)
```bash
# Миттєвий HTTPS-тунель на 3000 порт без встановлення утиліт
npx untun@latest tunnel --port 3000
```
=== Cloudflare CLI (cloudflared)
```bash
# Встановлення через brew на macOS
brew install cloudflared
cloudflared tunnel --url http://localhost:3000
```
=== ngrok
```bash
# Якщо використовуєте ngrok
ngrok http 3000
```
:::

Po uruchomieniu otrzymasz tymczasowy publiczny adres HTTPS, np.:
`https://your-tunnel-name.trycloudflare.com`

### 8.2. Konfiguracja webhooka do testów lokalnych
W kodzie tworzenia faktury podczas developmentu wskaż otrzymany adres:
```typescript
webHookUrl: "https://your-tunnel-name.trycloudflare.com/api/payment/webhook"
```
Teraz przy płatności testowej bank wyśle prawdziwy webhook prosto na Twój lokalny serwer w terminalu, a Ty będziesz mógł upewnić się, że podpis `x-sign` pomyślnie przechodzi weryfikację.

---

## 9. UX strony powrotnej i rozwiązywanie race condition

Gdy klient opłaca rachunek przez aplikację Monobank lub Apple Pay, bank natychmiast przekierowuje go na adres `redirectUrl` (`/payment-result?ref=...`).

Jednakże webhook sieciowy z serwera banku do Twojego serwera może opóźnić się o 1–2 sekundy ze względu na trasowanie sieciowe. Jeśli strona wyniku natychmiast sprawdzi status w bazie danych, ryzykuje wyświetlenie komunikatu: *„Zamówienie nieopłacone”*, co wywoła panikę u klienta (środki zostały pobrane z karty, a strona informuje o braku płatności).

### 9.1. Wzorzec inżynierski rozwiązania race condition
1. **Stan początkowy ładowania:** Strona otwiera się z neutralnym statusem: `„Potwierdzamy płatność w banku...”` oraz animowanym wskaźnikiem.
2. **Krótki polling (Short Polling):** Frontend wykonuje do 5 szybkich zapytań do własnego API co 1.5 sekundy (`/api/orders/check-status?ref=...`), czekając, aż webhook zmieni status zamówienia w bazie na `success`.
3. **Fallback:** Jeśli w ciągu 8 sekund webhook nie dotarł, klient widzi komunikat: *„Płatność przyjęta do przetworzenia. Gdy tylko bank potwierdzi transakcję, dostęp zostanie otwarty automatycznie”*.

### 9.2. Gotowy komponent React strony wyniku

```tsx
// app/payment-result/page.tsx
"use client";

import { useEffect, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";

export default function PaymentResultPage() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const ref = searchParams.get("ref");

  const [status, setStatus] = useState<"checking" | "success" | "pending" | "failed">("checking");

  useEffect(() => {
    if (!ref) {
      setStatus("failed");
      return;
    }

    let attempts = 0;
    const maxAttempts = 5;

    const interval = setInterval(async () => {
      attempts++;
      try {
        const res = await fetch(`/api/orders/status?ref=${encodeURIComponent(ref)}`);
        const data = await res.json();

        if (data.status === "success") {
          clearInterval(interval);
          setStatus("success");
        } else if (attempts >= maxAttempts) {
          clearInterval(interval);
          // Якщо вебхук затримався, переводимо в статус очікування, а не помилки
          setStatus(data.status === "processing" ? "pending" : "pending");
        }
      } catch (err) {
        if (attempts >= maxAttempts) {
          clearInterval(interval);
          setStatus("pending");
        }
      }
    }, 1500);

    return () => clearInterval(interval);
  }, [ref]);

  return (
    <div className="max-w-md mx-auto my-16 p-8 rounded-2xl bg-neutral-900 border border-neutral-800 text-center text-white">
      {status === "checking" && (
        <div>
          <div className="w-12 h-12 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
          <h2 className="text-xl font-semibold mb-2">Підтверджуємо оплату...</h2>
          <p className="text-sm text-neutral-400">Отримуємо статус транзакції від банку. Зачекайте кілька секунд.</p>
        </div>
      )}

      {status === "success" && (
        <div>
          <div className="w-12 h-12 bg-emerald-500/20 text-emerald-400 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl font-bold">✓</div>
          <h2 className="text-xl font-semibold mb-2">Оплата успішна!</h2>
          <p className="text-sm text-neutral-400 mb-6">Ваше замовлення #{ref} успішно підтверджено.</p>
          <button onClick={() => router.push("/dashboard")} className="px-6 py-2.5 rounded-xl bg-amber-500 hover:bg-amber-400 text-black font-semibold transition">
            Перейти до кабінету
          </button>
        </div>
      )}

      {status === "pending" && (
        <div>
          <div className="w-12 h-12 bg-amber-500/20 text-amber-400 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl font-bold">⏳</div>
          <h2 className="text-xl font-semibold mb-2">Платіж в обробці</h2>
          <p className="text-sm text-neutral-400 mb-6">Кошти зарезервовано. Підтвердження надійде протягом 1–2 хвилин.</p>
          <button onClick={() => router.push("/")} className="px-6 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-white font-medium transition">
            На головну
          </button>
        </div>
      )}
    </div>
  );
}
```

---

## 10. Zaawansowane możliwości: wbudowane pRRО, holdy i Wallet

Monobank Acquiring oferuje pełen zakres narzędzi dla bardziej złożonych modeli biznesowych:

![Końcowa strona płatności Monobank Hosted Checkout z Apple Pay i kartami](/api/guides-media/automation/monobank-acquiring-vibecoding/images/06-monobank-checkout-page.webp)

### 10.1. Programowe RRO: darmowy wbudowany Checkbox w Kasie Monobank

Dla większości ukraińskich JDG grup 2. i 3. fiskalizacja sprzedaży online jest wymogiem prawnym.

Główną przewagą Monobank dla przedsiębiorców jest **bezpłatna wbudowana integracja z serwisem pRRО Checkbox**:
- **Aktywacja jednym kliknięciem:** W panelu `web.monobank.ua` przejdź do ustawień utworzonego terminala i aktywuj przełącznik **„Fiskalizacja przez Checkbox”**. Monobank sam, bezpłatnie, tworzy kasę i podpisuje paragony Twoim KEP (Elektronicznym Podpisem Kwalifikowanym).
- **Automatyczna fiskalizacja:** Jeśli masz standardowy katalog towarów ze stałą stawką, nawet nie musisz zmieniać kodu tworzenia płatności – paragon jest automatycznie formułowany na podstawie pola `destination` w opisie płatności.
- **Rozszerzona fiskalizacja przez API:** Jeśli sprzedajesz towary z różnymi stawkami VAT lub pozycje akcyzowe z kodami UKT ZEД, przesyłaj tablicę `basketOrder` wewnątrz obiektu `merchantPaymInfo`:

```json
"merchantPaymInfo": {
  "reference": "order_1001",
  "destination": "Оплата онлайн-курсу",
  "customerEmails": ["client@example.com"],
  "basketOrder": [
    {
      "name": "Доступ до курсу з вайбкодингу",
      "qty": 1,
      "sum": 99000,
      "code": "SKU-COURSE-01",
      "unit": "шт.",
      "total": 99000
    }
  ]
}
```

### 10.2. Dwuetapowa płatność (Hold)

Jeśli sprzedajesz towary fizyczne, które mogą być niedostępne w magazynie, użyj trybu holdowania:

- **Blokada:** Przekaż `paymentType: "hold"` podczas tworzenia faktury. Środki są zamrażane na karcie kupującego na okres do 9 dni.
- **Pobranie (Finalize):** Po sprawdzeniu dostępności towaru wywołaj `/api/merchant/invoice/finalize`. Możesz pobrać zarówno pełną kwotę, jak i mniejszą (np. jeśli brakowało jednej pozycji).
- **Anulowanie:** Jeśli towaru nie ma, wywołaj `/api/merchant/invoice/cancel` – płatność jest anulowana bez żadnych prowizji bankowych dla kupującego.

### 10.3. Zapisywanie kart i subskrypcje (Wallet)

Jeśli uruchamiasz serwis SaaS z regularną miesięczną subskrypcją, przekaż `saveCardData: true` podczas tworzenia pierwszej faktury. Po pomyślnej płatności w webhooku zwrócony zostanie `walletId`. Używaj tego tokenu do następnych automatycznych pobrań bez ponownego wpisywania danych karty przez klienta.

---

## 11. Macierz bezpieczeństwa i testy automatyczne (Vitest / Jest)

Płatności to strefa maksymalnej odpowiedzialności finansowej. Błąd w zwykłym przycisku powoduje irytację, ale błąd w bramce płatniczej prowadzi do bezpośrednich strat materialnych lub blokady kasy przez bank.

Przed przejściem w tryb produkcyjny uruchom macierz testów bezpieczeństwa:

| Test bezpieczeństwa | Co dokładnie jest sprawdzane | Oczekiwane zachowanie systemu |
| :--- | :--- | :--- |
| **1. Ochrona przed zmianą ceny (Price Tampering)** | Klient wysyła `productId: "vip"`, ale próbuje podmienić w żądaniu `amount: 100` (1 UAH) | Serwer ignoruje pole `amount` od klienta, pobiera realną cenę z konfiguracji (2490 UAH = 249 000 kopiejek). Jeśli `productId` jest sfałszowany – zwraca `HTTP 400`. |
| **2. Blokada bez podpisu** | Na endpoint `/api/payment/webhook` nadchodzi żądanie POST bez nagłówka `x-sign` | Żądanie jest natychmiast blokowane odpowiedzią `HTTP 400 Bad Request`. Nie następuje żadna obróbka zamówienia. |
| **3. Blokada fałszywych podpisów** | Atakujący wysłał fake'owy podpis w `x-sign` ze statusem `status: "success"` | Weryfikacja `crypto.verify(SHA256, ...)` zwraca `false`. Serwer zwraca `HTTP 400/401`, dostęp lub towar nie są wydawane. |
| **4. Idempotentność webhooka (Duplicate Delivery)** | Monobank wysłał ten sam webhook `success` dwukrotnie lub trzykrotnie z powodu opóźnienia sieciowego | Dostęp lub towar są aktywowane **tylko raz**. Powtórzony webhook nie powoduje duplikacji wydania, zwracając bankowi `HTTP 200 OK`. |
| **5. Odporność na race condition** | Webhook od banku dotarł szybciej niż zakończyło się pierwotne żądanie HTTP tworzenia faktury w bazie | Handler webhooka jest odporny na brak rekordu w DB (używa `UPSERT` lub tworzy zamówienie „w locie”). |
| **6. Przestrzeganie rate-limitów (Rate Limit Buffer)** | Przy awarii webhooka uruchamia się polling statusu `/api/merchant/invoice/status` | Zapytania są wykonywane z pauzą **nie mniejszą niż 15 sekund**, co chroni terminal przed blokadą `HTTP 429 Too Many Requests`. |

### 11.1. Gotowy zestaw testów automatycznych dla Twojego projektu

Poproś swojego agenta AI o dodanie następującego zestawu testów do projektu (`monobank-acquiring.test.ts`):

```typescript
import { describe, it, expect } from "vitest";
import crypto from "crypto";

// Генерація тестової пари ECDSA secp256r1 для симуляції підписів Monobank
const { publicKey, privateKey } = crypto.generateKeyPairSync("ec", {
  namedCurve: "prime256v1",
  publicKeyEncoding: { type: "spki", format: "pem" },
  privateKeyEncoding: { type: "pkcs8", format: "pem" },
});

describe("Monobank Acquiring Security Tests", () => {
  it("Тест 1: Не дозволяє клієнту маніпулювати ціною", () => {
    const CATALOG: Record<string, { priceUah: number }> = { plan_pro: { priceUah: 990 } };
    const clientPayload = { productId: "plan_pro", amount: 100 }; // Спроба підсунути 1 грн
    
    const safeAmount = Math.round(CATALOG[clientPayload.productId].priceUah * 100);
    expect(safeAmount).toBe(99000); // 990.00 грн, а не 1.00 грн
  });

  it("Тест 2: Відхиляє вебхук без обов'язкового заголовка x-sign", () => {
    const headers: Record<string, string> = {};
    const hasSignature = Boolean(headers["x-sign"]);
    expect(hasSignature).toBe(false);
  });

  it("Тест 3: Успішно верифікує валідний цифровий підпис банку", () => {
    const rawPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 99000 });
    
    const signer = crypto.createSign("SHA256");
    signer.update(rawPayload);
    const validSignatureBase64 = signer.sign(privateKey, "base64");

    const verifier = crypto.createVerify("SHA256");
    verifier.update(rawPayload);
    const isValid = verifier.verify(publicKey, Buffer.from(validSignatureBase64, "base64"));

    expect(isValid).toBe(true);
  });

  it("Тест 4: Блокує підроблений підпис або модифіковане тіло запиту", () => {
    const originalPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 99000 });
    const tamperedPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 1000 });

    const signer = crypto.createSign("SHA256");
    signer.update(originalPayload);
    const signature = signer.sign(privateKey, "base64");

    const verifier = crypto.createVerify("SHA256");
    verifier.update(tamperedPayload);
    const isValid = verifier.verify(publicKey, Buffer.from(signature, "base64"));

    expect(isValid).toBe(false);
  });

  it("Тест 5: Ідемпотентність — повторний вебхук не видає товар двічі", () => {
    const processedOrders = new Set<string>();
    
    function handleOrder(invoiceId: string): { processed: boolean } {
      if (processedOrders.has(invoiceId)) {
        return { processed: false }; // Дубль відхилено
      }
      processedOrders.add(invoiceId);
      return { processed: true };
    }

    expect(handleOrder("inv_001").processed).toBe(true);  // Перший вебхук
    expect(handleOrder("inv_001").processed).toBe(false); // Повторний прихід дубліката
    expect(processedOrders.size).toBe(1);
  });
});
```

---

## 12. Finalna checklista inżynierska przed startem

Sprawdź swój moduł płatniczy według tych 10 punktów przed przełączeniem na płatności produkcyjne:

- [ ] **Compliance strony:** W stopce dodano linki do Publicznej Oferty, Polityki Prywatności, Warunków Zwrotu oraz pełne dane JDG z NIP.
- [ ] **Kwoty w kopiejkach:** Wszystkie wartości `amount` pomnożone przez 100 ($1\text{ UAH} = 100\text{ kop}$) przez `Math.round`.
- [ ] **Bezpieczeństwo klucza:** Token wyniesiony do `.env` pod nazwą `MONOBANK_TOKEN` i dodany do `.gitignore`.
- [ ] **Ceny z backendu (SSOT):** Klient przekazuje tylko identyfikator produktu, kwota jest pobierana wyłącznie z bazy lub konfiguracji.
- [ ] **Obsługa rawBody:** Webhook weryfikuje oryginalny bufor tekstowy lub binarny (`req.text()` lub `req.rawBody`), unikając podwójnego parsowania przez `JSON.stringify`.
- [ ] **Zabezpieczenie kryptograficzne:** Webhook weryfikuje podpis `x-sign` za pomocą klucza publicznego banku algorytmem `SHA256` ECDSA.
- [ ] **Publiczny URL dla webhooka:** Serwer jest dostępny z zewnątrz przez ważny certyfikat SSL HTTPS (do testów lokalnych pasuje Cloudflare Tunnel lub ngrok).
- [ ] **Idempotentność:** Ponowne nadejście tego samego webhooka nie powoduje podwójnego wydania towaru lub subskrypcji.
- [ ] **UX strony wyniku:** Na `/payment-result` skonfigurowano loader oczekiwania i krótki polling statusu, aby chronić użytkownika przed race condition.
- [ ] **Realna płatność 1–5 UAH:** Przeprowadzono udaną transakcję testową prawdziwą kartą, sprawdzono pobranie środków i wyświetlenie w panelu bankowym.

---

## Przydatne zasoby i pliki do pobrania

- [Pobierz pakiet skillsów monobank-acquiring.zip (38 KB) →](/downloads/monobank-acquiring.zip)
- [Oficjalna strona Promtów AI Monobank →](https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts)
- [Panel osobisty Monobank Business →](https://web.monobank.ua/)
- [Praktyczne wideo na YouTube z pełnym rozbiorem flow →](https://youtu.be/GMh_fOCiQ4E)
- [Portal testowy API Monobank →](https://api.monobank.ua/)