# Monobank-Acquiring ohne Entwickler per Vibe Coding integrieren

> Schritt-für-Schritt-Anleitung zur Integration von Monobank-Online-Zahlungen auf einer Website: Compliance für Einzelunternehmer (FOP), Terminal-Einrichtung, ECDSA-Webhook-Verifizierung, zuverlässige UX und Sicherheitstests.

Vibe Coding hat die Entwicklung digitaler Produkte grundlegend verändert. Heute ist es nicht mehr nötig, einen Backend-Entwickler einzustellen oder sich selbst in kryptografische Protokolle einzuarbeiten, um auf einer Website Zahlungen über Apple Pay, Google Pay oder Bankkarten zu akzeptieren. Es reicht aus, die Geschäftslogik klar gegenüber einem AI-Agenten (Codex, Antigravity, Cursor oder Claude Code) zu formulieren und ihm den richtigen Kontext der technischen Dokumentation bereitzustellen.

Dieses Handbuch behandelt den gesamten Lebenszyklus der Anbindung von Monobank Internet-Acquiring an Ihr Projekt: vom Durchlaufen des Bank-Compliance-Prozesses und der Einrichtung des Unternehmer-Kontos bis hin zur Generierung eines sicheren Zahlungs-Gateways mit kryptografischer Verifizierung der ECDSA-Signaturen in Webhooks, der Behandlung von Race Conditions im Interface sowie dem Setzen obligatorischer automatisierter Tests.

> [!TIP]
> **Video-Version des Handbuchs:** Wenn Sie eine visuelle Darstellung bevorzugen, sehen Sie sich das [detaillierte Praxis-Video auf YouTube →](https://youtu.be/GMh_fOCiQ4E) an, in dem der gesamte Prozess live am Bildschirm gezeigt wird – von der ersten Anfrage bis zur tatsächlichen Abbuchung.

> [!NOTE]
> **Agent Skills-Paket:** Für maximale Genauigkeit bei der Codegenerierung laden Sie das offizielle Wissenspaket für Ihren AI-Assistenten herunter:  
> [Vollständiges Archiv monobank-acquiring.zip herunterladen (38 KB) →](/downloads/monobank-acquiring.zip)

---

## 1. Architektur des Hosted Checkout und Zahlungslebenszyklus

Monobank Acquiring funktioniert nach dem Schema **Hosted Checkout** (Zahlung auf einer geschützten Bankseite). Das bedeutet, dass Ihre Website keine komplexe und teure PCI DSS-Sicherheitszertifizierung benötigt, da der Benutzer die Kartendaten direkt auf der geschützten Domain der Bank eingibt.

Die offizielle Monobank-Dokumentation für AI-Tools ist unter [monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts →](https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts) verfügbar. Speichern Sie diesen Link als kanonische Erstquelle für die technischen Anforderungen der Bank.

### 1.1. Grundlegender Zahlungsfluss

```
[ Клієнт на сайті ]
       │
       ├─ 1. Натискає «Оплатити»
       ▼
[ Ваш Сервер ] ──────────────► [ Monobank API: /invoice/create ]
       ▲                              │
       │  отримує pageUrl, invoiceId  │
       └──────────────────────────────┘
       │
       ├─ 2. Редирект клієнта на pageUrl (Hosted Checkout)
       ▼
[ Платіжна сторінка Monobank ] ───► Оплата (Apple Pay / Google Pay / Картка)
       │
       ├─ 3. Асинхронний POST вебхук із підписом x-sign
       ▼
[ Ваш Вебхук-ендпоінт ] ─────────► Перевірка ECDSA SHA-256 → Статус "success"
       │
       ├─ 4. Клієнт повертається на /payment-result
       ▼
[ Сторінка успіху ] ────────────► Відображення доступу / чека
```

- **1. Erstellung der Rechnung (`invoice/create`):** Der Kunde wählt auf Ihrer Website ein Produkt oder einen Tarif aus. Der Server-Handler sendet eine `POST`-Anfrage an die Monobank API mit Betrag, Verwendungszweck der Zahlung und Rückleitungsadresse. Die Bank gibt eine eindeutige `invoiceId` und den Link `pageUrl` zurück.
- **2. Umleitung zur Zahlungsseite:** Der Kunde wird auf die `pageUrl` weitergeleitet, wo er die Bestellung über Apple Pay, Google Pay, die Monobank-App oder durch Eingabe der Bankkartendaten bezahlt.
- **3. Empfang des Webhooks:** Nach Abschluss der Zahlung sendet Monobank automatisch eine `POST`-Anfrage an Ihre vorab konfigurierte `webHookUrl` mit dem Transaktionsstatus und einer kryptografischen digitalen Signatur.
- **4. Statusfixierung (Terminal States):** Ihr Server muss auf zwei finale Status reagieren: `success` (Zahlung erfolgreich, Zugriff gewähren oder Bestellung versenden) und `failure` (Fehler oder Ablehnung der Zahlung durch die Bank).

> [!IMPORTANT]
> Der Status `expired` (Ablaufdatum des Links überschritten) **löst keinen Webhook aus**. Wenn der Kunde die Zahlungsseite schließt und nicht zahlt, muss der Bestellstatus über ein Backup-Polling der API überprüft werden.

---

## 2. Rechtliche Voraussetzungen und Compliance-Checkliste für Einzelunternehmer (FOP)

Selbst wenn der Zahlungscode fehlerfrei geschrieben ist, aktiviert der Finanzüberwachungs- und Sicherheitsdienst von Monobank die Live-Zahlungsannahme nicht, wenn die Website nicht den Anforderungen der ukrainischen Gesetzgebung und den Regeln der Zahlungssysteme Visa/Mastercard entspricht.

### 2.1. Bankvoraussetzungen
- **Offenes FOP- oder juristisches Personenkonto bei Monobank:** Acquiring wird ausschließlich an Geschäftskonten angebunden. Die Annahme kommerzieller Zahlungen auf persönliche schwarze oder weiße Karten von Privatpersonen ist gesetzlich verboten.
- **Passende KVED-Codes:** In den Registrierungsdaten des FOP müssen Codes für Online-Handel oder Dienstleistungen angegeben sein (z. B. `47.91` – Einzelhandel über das Internet, `62.01`/`62.02` – Computerprogrammierung und Beratung, `85.59` – Sonstige Bildungsformen).

### 2.2. Obligatorische Checkliste der Seiten vor Einreichung zur Moderation

Bevor Sie den Antrag auf Aktivierung des Internet-Terminals stellen, stellen Sie sicher, dass folgende Abschnitte auf Ihrer Website vorhanden sind (üblicherweise im Footer platziert):

- [ ] **Öffentliches Angebot (Public Offer Agreement):** Beschreibung des Vertragsgegenstands, Zeitpunkt des Vertragsschlusses, Rechte und Pflichten der Parteien.
- [ ] **Datenschutzrichtlinie (Privacy Policy):** Klare Formulierung darüber, welche Kundendaten gesammelt werden und wie sie gemäß dem Gesetz „Über den Schutz personenbezogener Daten“ geschützt werden.
- [ ] **Rückgabe- und Lieferbedingungen:** Verfahren für die Rückgabe von Waren oder Geld innerhalb von 14 Tagen gemäß dem Gesetz „Über den Schutz der Verbraucherrechte“, sowie für digitale Produkte/Abonnements – Regeln für den Widerruf der Dienstleistung.
- [ ] **Vollständige Unternehmensangaben im Website-Footer:** Name des FOP oder der GmbH, Steuer-ID / EDRPOU, juristische Adresse, Kontakttelefonnummer und funktionierende Support-E-Mail-Adresse.
- [ ] **Transparente Preise und Beschreibungen:** Jede Zahlungsschaltfläche muss einen festen Betrag in Hrywnja (UAH) und eine verständliche Beschreibung dessen enthalten, wofür der Kunde genau bezahlt.

> [!WARNING]
> Wenn auf der Website kein öffentliches Angebot, keine FOP-Angaben vorhanden sind oder statt Preisen Platzhalter wie „nach Vereinbarung“ stehen, lehnt der Sicherheitsdienst von Monobank die Registrierung des Terminals im Prüfprozess ab.

---

## 3. Erstellung des Web-Terminals im Monobank Business-Konto

Für die Interaktion mit der Zahlungs-API benötigen Sie einen persönlichen Zugriffsschlüssel – `X-Token`. Er wird kostenlos im persönlichen Unternehmerkonto generiert.

### 3.1. Schritt-für-Schritt-Algorithmus zur Kasseneröffnung

1. **Autorisierung:** Gehen Sie zu [web.monobank.ua →](https://web.monobank.ua/) und melden Sie sich per QR-Code über die Monobank-App auf Ihrem Smartphone an.
2. **Wechsel zur Kasse:** Wählen Sie im linken Navigationsmenü den Abschnitt **„Kasse“**.
3. **Tool hinzufügen:** Klicken Sie auf die Schaltfläche **„+ Tool hinzufügen“** und wählen Sie die Option **„Zahlung auf der Website (Eigenentwicklung)“**.
4. **Terminalregistrierung:** Geben Sie den Projektnamen ein (z. B. `Meine Website` oder `Hauptterminal`) und bestätigen Sie den Vorgang mit der Schaltfläche **„Verbinden“**.
5. **Generierung des API-Tokens:** Öffnen Sie das erstellte Terminal, wechseln Sie zum Reiter **„Integrationen / API Keys“**, klicken Sie auf **„Token erstellen“** und kopieren Sie den generierten Schlüssel.

> 📍 **Navigation im Konto:** `web.monobank.ua` → `Kasse` → `+ Tool hinzufügen` → `Zahlung auf der Website (Eigenentwicklung)`

![Erstellung des Zahlungstools im Monobank Kasse-Konto](/api/guides-media/automation/monobank-acquiring-vibecoding/images/01-monobank-kasa-add-terminal.webp)

> 📍 **Empfang des API-Schlüssels:** `Kasse` → `Ihr Terminal` → `Integration` → `X-Token erstellen`

![Modalfenster zur Erstellung und zum Kopieren des X-Token-Zugriffsschlüssels](/api/guides-media/automation/monobank-acquiring-vibecoding/images/02-monobank-create-token.webp)

### 3.2. Eiserne Regeln für die Token-Sicherheit

- **Kein Hardcoding im Client-Code:** `X-Token` gewährt direkten Zugriff auf die Verwaltung Ihrer Finanzen und Rückerstattungen. Es ist strengstens verboten, ihn in offenem JavaScript/HTML zu speichern oder in öffentlichen GitHub-Repositorien zu veröffentlichen.
- **Verwendung von Umgebungsvariablen:** Speichern Sie das Token ausschließlich in der Datei `.env` auf dem Server unter dem Namen `MONOBANK_TOKEN` oder im Secrets-Bereich Ihrer Hosting-Plattform (Vercel, Render, Railway, Replit, Lovable).
- **Test-Token für die Entwicklung:** Für erste Experimente stellt die Bank einen separaten Test-Token auf der Seite [api.monobank.ua →](https://api.monobank.ua/) bereit, der die Simulation von Transaktionen ohne echte Abbuchungen ermöglicht.

---

## 4. Offizielle Monobank AI-Prompts für Vibe-Coder

Das Monobank-Team hat einen Satz offizieller System-Prompts für AI-Agenten entwickelt. Ihr Hauptvorteil liegt in der exakten Übereinstimmung mit den aktuellen Endpunkten der Bank.

![Seite der offiziellen Monobank-Dokumentation für AI-Tools](/api/guides-media/automation/monobank-acquiring-vibecoding/images/03-monobank-ai-prompts-page.webp)

> [!WARNING]
> **Die wichtigste Anfängerfalle – Betrag in Kopeken:** Die Monobank API akzeptiert alle Beträge ausschließlich in minimalen Währungseinheiten (Kopeken). $100\text{ UAH} = 10\,000\text{ Kopeken}$. Wenn Sie `amount: 100` übergeben, zahlt der Kunde nur 1 Hrywnja.

### 4.1. Basis-Prompt zur Erstellung der Zahlung

Kopieren Sie diesen Prompt und senden Sie ihn im Chat Ihres AI-Assistenten:

```markdown
Я хочу додати на свій сайт можливість приймати платежі через Monobank.

ЗАДАЧА:
Напиши повний готовий код для створення платежу та перенаправлення користувача на оплату.

ЩО МАЄ СТАТИСЯ:
1. На моєму сайті кнопка "Перейти до оплати".
2. Користувач клацає → створюється платіж в Monobank.
3. Користувач переходить на сторінку оплати Monobank.
4. Після оплати він повертається на мій сайт (https://mysite.com/payment-result).
5. Я перевіряю статус платежу та показую результат.

ТЕСТОВІ ДАНІ:
- Сума: 100 грн (10000 копійок)
- Опис платежу: "Оплата замовлення"
- Куди повертати користувача: https://mysite.com/payment-result

МІЙ ТОКЕН MONOBANK:
Змінна оточення MONOBANK_TOKEN з файлу .env

ДОКУМЕНТАЦІЯ:
- Створення платежу: https://monobank.ua/api-docs/acquiring/methods/ia/post--api--merchant--invoice--create
- Перевірка статусу: https://monobank.ua/api-docs/acquiring/methods/ia/get--api--merchant--invoice--status

Напиши весь код для цього flow.
```

### 4.2. Prompt zur Einrichtung des Webhook-Handlers

Ohne Webhook erfährt der Server nicht von der erfolgreichen Zahlung, wenn der Kunde den Browser sofort nach der Abbuchung schließt:

```markdown
Мені потрібно, щоб мій сервер автоматично дізнавався, коли користувач оплатив замовлення.

ЗАДАЧА:
Налаштуй автоматичне отримання інформації про платежі від Monobank (webhook).

ЩО МАЄ СТАТИСЯ:
1. Користувач сплачує на сторінці Monobank.
2. Monobank автоматично надсилає POST-запит на мій сервер.
3. Мій сервер отримує дані: invoiceId, статус (success/failure), суму.
4. Сервер перевіряє криптографічний підпис (ECDSA SHA-256 через заголовок x-sign).
5. Оновлює статус замовлення в базі даних та логує результат.

ВАЖЛИВО:
- Обов'язково валідуй заголовок x-sign за допомогою публічного ключа банку.
- Обробляй сире тіло запиту (raw body Buffer / raw string), а не розпарсений JSON.
- Обробляй статуси: success, failure, processing, hold, expired.
- Забезпеч ідемпотентність: якщо один і той самий вебхук прийде двічі, не дублюй видачу товару.

Напиши весь код для надійного webhook-обробника.
```

---

## 5. Skills-Paket monobank-acquiring: Optimierung des Agenten

Wenn man sich auf einen kurzen Prompt beschränkt, schreibt der AI-Assistent einen Basiscode (ca. 6,8 von 10 Punkten): Der Button funktioniert zwar, aber der Code enthält keine Überprüfung der kryptografischen Signaturen, keinen Schutz gegen Preismanipulation und keine Behandlung seltener Netzwerkfehler.

Um Produktionsniveau (9,8–10 Punkte) zu erreichen, wird im Projektstammverzeichnis ein spezialisiertes Skills-Paket **`monobank-acquiring`** hinzugefügt.

![Audit der Zahlungsintegration: Vergleich der Sicherheit vor und nach Nutzung des Skills](/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. Anatomie und Struktur des Skills-Pakets

| Skill-Datei | Inhalt und zuständige Aufgaben |
| :--- | :--- |
| **`SKILL.md`** | **Zentrales Manifest:** Basis-Flow, `X-Token`-Autorisierung, Datentypen und Fehlerbehandlung für `400`, `403`, `429`, `500`. |
| **`quickstart.md`** | **Schnellstart:** Schritt-für-Schritt-Tutorial zur Erstellung einer Rechnung und zum Fallback-Polling mit fertigen `curl`-Befehlen. |
| **`invoice.md`** | **Rechnungslebenszyklus:** Endpunkte zur Erstellung der Rechnung, Statusprüfung, Stornierung und Ungültigerklärung von Links. |
| **`webhook.md`** | **Kryptografischer Schutz:** Mathematisch exakte Verifizierung der ECDSA SHA-256-Signatur aus dem Header `x-sign`. |
| **`payment.md`** | **Direkte Zahlungen:** Abbuchung mit gespeichertem Kartentoken, synchrone Transaktionen und 3DS-Prüfung. |
| **`wallet.md`** | **Kartentokenisierung (Wallet):** Sichere Speicherung des Zahlungsmittels des Kunden im Bankspeicher für One-Click-Zahlungen. |
| **`fiscal.md`** | **Quittungen und pRRO:** Beschreibung der Warenkorbstruktur `basketOrder`, Berechnung von Steuern, Rabatten und Export der Quittungen als PDF. |
| **`statement.md`** | **Kontoauszüge und Analysen:** Abruf des Registers erfolgreicher Operationen für den gewählten Zeitraum inklusive Berechnung der Bankgebühren. |
| **`merchant.md`** | **Händlerdaten:** Abruf des öffentlichen Schlüssels der Bank, Verwaltung von Sub-Merchants und Kassierern. |
| **`examples/`** | **Fertige Server:** Arbeitsbeispiele für Server in 6 populären Sprachen (Node.js, Python, Go, PHP, C#, Java). |

---

## 6. Praktische Umsetzung: Dynamischer Zahlungsfluss

Jedes Projekt hat seine eigene Struktur: Manche verkaufen digitale Beratungen mit festen Tarifen, andere haben einen Online-Shop mit dynamischem Warenkorb, wieder andere einen einfachen Landing Page mit einer einzigen Spenden- oder Abonnement-Schaltfläche.

Der häufigste Anfängerfehler besteht darin, den Betrag (z. B. 1000 UAH) hart in den Client-Code des Buttons oder in den Body der POST-Anfrage vom Browser einzubetten. Dieser Ansatz erzeugt eine kritische Sicherheitslücke.

### 6.1. Sicherheitsprinzip: Dynamischer Preis statt Hardcoding

- **Vertrauen Sie niemals dem Betrag aus dem Browser:** Wenn das Client-JavaScript `{ price: 1000 }` an den Server sendet, kann ein Angreifer über den Code-Inspektor (DevTools) oder Postman diesen Wert auf `{ price: 1 }` ändern und den Zugriff für 1 Hrywnja erwerben.
- **Server als einzige Wahrheitsquelle (Single Source of Truth):** Das Frontend übergibt an das Backend nur die Produkt-ID (`productId`), den gewählten Tarif (`planId: "pro"`) oder ein Array von Warenkorb-IDs (`items: [{ id: "book_1", qty: 2 }]`).
- **Automatische Konvertierung in Kopeken:** Der Server ruft den aktuellen Preis aus der Konfigurationsdatei oder Datenbank ab und multipliziert ihn selbstständig mit 100:
  $$\text{amount} = \text{Math.round}(\text{realPrice} \times 100)$$
  Wenn Sie den Preis in der Datenbank ändern oder einen Rabatt ankündigen, wird die Monobank-Rechnung mit dem neuen korrekten Betrag generiert, ohne dass Sie in das Zahlungs-Gateway eingreifen müssen.

### 6.2. Universeller AI-Prompt zur Anpassung an jedes Projekt

Kopieren Sie diesen Prompt und senden Sie ihn im Chat Ihres AI-Agenten (Codex, Antigravity, Cursor oder Claude Code). Der Agent scannt selbst die Dateien Ihrer Website, findet vorhandene Buttons und Preise und erstellt eine robuste Integration:

```markdown
У корінь нашого проєкту додано офіційний пакет знань Monobank Acquiring у папку /monobank-acquiring.

ЗАДАЧА:
1. Проаналізуй архітектуру нашого проєкту: знайди, де у нас описані товари, послуги, тарифи чи кнопки оформлення замовлення/оплати.
2. Створи або інтегруй захищений серверний ендпоінт створення платежу Monobank:
   - Клієнт надсилає ТІЛЬКИ ідентифікатор товару/тарифу (наприклад, tariffId або productId), але НЕ суму.
   - Сервер знаходить реальну актуальну ціну з нашого конфігу чи бази даних і переводить її у копійки (ціна * 100).
   - Токен береться безпечно з process.env.MONOBANK_TOKEN.
   - Робиться POST-запит до https://api.monobank.ua/api/merchant/invoice/create.
   - Клієнту повертається посилання pageUrl для переходу на чекаут банку.
3. Онови наші існуючі кнопки оплати на фронтенді:
   - Додай статус завантаження (індикатор Loader та блокування кнопки від повторного кліку).
   - При успішній відповіді здійснюй плавний редирект користувача на отримане посилання Monobank.
   - Додай обробку помилок з інтуїтивними сповіщеннями для користувача.
4. Створи сторінку результату оплати (/payment-result), яка перевіряє статус замовлення та інформує користувача про успіх.
5. Напиши юніт-тести для перевірки динамічного розрахунку сум і створення інвойсу.

Усі технічні вимоги, структури запитів та коди валюти бери з файлів у папці /monobank-acquiring (особливо SKILL.md та invoice.md).
```

![Arbeit des AI-Agenten in der IDE mit Analyse der Struktur und Erstellung des dynamischen Endpunkts](/api/guides-media/automation/monobank-acquiring-vibecoding/images/04-codex-initial-prompt.webp)

### 6.3. Architekturmuster des Backends zur Rechnungsstellung

Hier sind geprüfte Server-Implementierungen für die beiden beliebtesten Web-Stacks:

:::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. Sichere Webhook-Verarbeitung und kryptografische ECDSA-Signatur

Der verantwortungsvollste Teil jeder finanziellen Integration ist die Verifizierung der Zahlungsbenachrichtigungen. Ein Angreifer könnte eine gefälschte HTTP-Anfrage an Ihre Adresse `/api/payment/webhook` mit einer falschen Erfolgsmeldung senden.

Um dies zu verhindern, signiert Monobank jeden Webhook mit dem asymmetrischen Algorithmus **ECDSA (Kurve secp256r1 / SHA-256)** und übergibt die Signatur im HTTP-Header `x-sign`.

### 7.1. Warum `JSON.stringify` die Signaturverifizierung bricht

> [!CAUTION]
> **Kritische `rawBody`-Falle:** Für die Überprüfung der ECDSA-Signatur wird der **strikt originale Byte-Stream** benötigt, den der Monobank-Server gesendet hat. Wenn Sie versuchen, JSON zu parsen und erneut `JSON.stringify(req.body)` aufzurufen, ändern sich die Reihenfolge der Schlüssel, Leerzeichen oder Zeilenumbrüche. Dies führt zu einem anderen SHA-256-Hash, und die Signaturprüfung **garantiert einen Fehler**!

### 7.2. Implementierung der Webhook-Verifizierung

Unten finden Sie die korrekte Implementierung zum Abrufen des rohen Bodies und zur Verifizierung für Next.js App Router und 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. Lokales Testen von Webhooks (Localhost & Cloudflare Tunnels)

Ein verbreitetes Problem bei Vibe-Codern: Beim Starten des Servers auf `http://localhost:3000` kann die Bank den Webhook nicht zustellen, da der lokale Rechner keine öffentliche IP-Adresse im Internet hat.

Monobank sendet Webhooks ausschließlich an öffentliche Adressen mit gültigem `HTTPS`-Protokoll. Um den vollständigen Zyklus auf Ihrem Computer zu testen, richten Sie einen sicheren Tunnel ein.

### 8.1. Schneller Tunnel-Start (ohne Registrierung und kostenlos)

Nutzen Sie Cloudflare Tunnel (`cloudflared`) oder `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
```
:::

Nach dem Start erhalten Sie eine temporäre öffentliche HTTPS-Adresse, z. B.:
`https://your-tunnel-name.trycloudflare.com`

### 8.2. Einstellung des Webhooks für lokale Tests
Geben Sie im Code zur Rechnungsstellung während der Entwicklung die erhaltene Adresse an:
```typescript
webHookUrl: "https://your-tunnel-name.trycloudflare.com/api/payment/webhook"
```
Nun sendet die Bank bei einer Testzahlung den echten Webhook direkt an Ihren lokalen Server im Terminal, und Sie können sicherstellen, dass die Signatur `x-sign` die Prüfung erfolgreich passiert.

---

## 9. UX der Rückleitungsseite und Lösung der Race Condition

Wenn der Kunde die Rechnung über die Monobank-App oder Apple Pay bezahlt, leitet die Bank ihn sofort auf die `redirectUrl` (`/payment-result?ref=...`) weiter.

Der Netzwerk-Webhook vom Bankserver zu Ihrem Server kann jedoch aufgrund von Netzwerk-Routings um 1–2 Sekunden verzögert sein. Wenn die Ergebnis-Seite den Status sofort in der Datenbank prüft, riskiert sie die Anzeige: *„Bestellung nicht bezahlt“*, was beim Kunden Panik auslöst (Geld wurde von der Karte abgebucht, aber die Website meldet fehlende Zahlung).

### 9.1. Ingenieursmuster zur Lösung der Race Condition
1. **Initialer Ladestatus:** Die Seite öffnet sich mit neutralem Status: *„Zahlung bei der Bank bestätigen...“* und einem animierten Indikator.
2. **Kurzes Polling (Short Polling):** Das Frontend führt bis zu 5 schnelle Anfragen an die eigene API alle 1,5 Sekunden durch (`/api/orders/check-status?ref=...`), während darauf gewartet wird, dass der Webhook den Bestellstatus in der Datenbank auf `success` ändert.
3. **Fallback:** Wenn der Webhook nach 8 Sekunden nicht eingetroffen ist, sieht der Kunde die Meldung: *„Zahlung zur Bearbeitung angenommen. Sobald die Bank die Transaktion bestätigt, wird der Zugriff automatisch freigeschaltet.“*

### 9.2. Fertige React-Komponente der Ergebnis-Seite

```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. Erweiterte Funktionen: Integriertes pRRO, Hold und Wallet

Monobank Acquiring bietet ein umfassendes Spektrum an Tools für komplexere Geschäftsmodelle:

![Finale Monobank Hosted Checkout-Zahlungsseite mit Apple Pay und Karten](/api/guides-media/automation/monobank-acquiring-vibecoding/images/06-monobank-checkout-page.webp)

### 10.1. Software-RRO: Kostenloser integrierter Checkbox in der Monobank Kasse

Für die meisten ukrainischen FOPs der Gruppen 2 und 3 ist die Fiscalisierung von Online-Verkäufen eine gesetzliche Anforderung.

Der Hauptvorteil von Monobank für Unternehmer ist die **kostenlose integrierte Integration mit dem pRRO-Dienst Checkbox**:
- **Aktivierung mit einem Klick:** Im Konto `web.monobank.ua` gehen Sie zu den Einstellungen des erstellten Terminals und aktivieren den Schalter **„Fiscalisierung über Checkbox“**. Monobank erstellt die Kasse kostenlos selbst und signiert die Quittungen mit Ihrer KEP.
- **Automatische Fiscalisierung:** Wenn Sie einen Standard-Warenkatalog mit einheitlichem Steuersatz haben, müssen Sie den Code zur Zahlungsstellung nicht einmal ändern – die Quittung wird automatisch basierend auf dem Feld `destination` im Verwendungszweck gebildet.
- **Erweiterte Fiscalisierung über API:** Wenn Sie Waren mit unterschiedlichen MwSt-Sätzen oder akzisepflichtige Positionen mit UKTZED-Codes verkaufen, übergeben Sie das Array `basketOrder` innerhalb des Objekts `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. Zweistufige Zahlung (Hold)

Wenn Sie physische Waren verkaufen, die möglicherweise nicht auf Lager sind, nutzen Sie den Hold-Modus:

- **Blockierung:** Übergeben Sie `paymentType: "hold"` bei der Erstellung der Rechnung. Das Geld wird auf der Karte des Käufers für bis zu 9 Tage eingefroren.
- **Abbuchung (Finalize):** Nach Prüfung der Verfügbarkeit rufen Sie `/api/merchant/invoice/finalize` auf. Sie können den vollen Betrag oder einen geringeren abbuchen (z. B. wenn eine Position fehlt).
- **Stornierung:** Wenn die Ware nicht verfügbar ist, rufen Sie `/api/merchant/invoice/cancel` auf – die Zahlung wird storniert, ohne dass für den Käufer Bankgebühren anfallen.

### 10.3. Kartenspeicherung und Abonnements (Wallet)

Wenn Sie einen SaaS-Dienst mit regelmäßiger monatlicher Abonnementgebühr starten, übergeben Sie `saveCardData: true` bei der Erstellung der ersten Rechnung. Nach erfolgreicher Zahlung wird im Webhook die `walletId` zurückgegeben. Nutzen Sie dieses Token für zukünftige automatische Abbuchungen, ohne dass der Kunde die Kartendaten erneut eingeben muss.

---

## 11. Sicherheitsmatrix und automatische Tests (Vitest / Jest)

Zahlungen sind der Bereich maximaler finanzieller Verantwortung. Ein Fehler in einem normalen Button führt zu Ärgernis, aber ein Fehler im Zahlungs-Gateway führt zu direkten materiellen Verlusten oder zur Sperrung der Kasse durch die Bank.

Führen Sie vor dem Wechsel in den Live-Betrieb die Sicherheits-Testmatrix durch:

| Sicherheitstest | Was genau geprüft wird | Erwartetes Systemverhalten |
| :--- | :--- | :--- |
| **1. Schutz vor Preisänderung (Price Tampering)** | Der Kunde sendet `productId: "vip"`, versucht aber, im Request `amount: 100` (1 UAH) zu manipulieren | Der Server ignoriert das Feld `amount` vom Client, nimmt den realen Preis aus der Konfiguration (2490 UAH = 249 000 Kopeken). Wenn `productId` gefälscht ist – gibt `HTTP 400` zurück. |
| **2. Blockierung ohne Signatur** | Auf den Endpunkt `/api/payment/webhook` kommt eine POST-Anfrage ohne Header `x-sign` | Die Anfrage wird sofort mit Antwort `HTTP 400 Bad Request` blockiert. Keine Bestellverarbeitung findet statt. |
| **3. Blockierung gefälschter Signaturen** | Ein Angreifer hat eine Fake-Signatur in `x-sign` mit Status `status: "success"` gesendet | Die Prüfung `crypto.verify(SHA256, ...)` gibt `false` zurück. Der Server antwortet mit `HTTP 400/401`, Zugriff oder Ware werden nicht gewährt. |
| **4. Idempotenz des Webhooks (Duplicate Delivery)** | Monobank hat denselben `success`-Webhook zweimal oder dreimal hintereinander wegen Netzwerk-Lag gesendet | Zugriff oder Ware werden **nur einmal** aktiviert. Der wiederholte Webhook löst keine doppelte Ausgabe aus und gibt der Bank `HTTP 200 OK` zurück. |
| **5. Resistenz gegen Race Conditions** | Der Webhook der Bank kam ein, bevor die ursprüngliche HTTP-Anfrage zur Rechnungsstellung in der DB abgeschlossen war | Der Webhook-Handler ist resistent gegen fehlende Datensätze in der DB (nutzt `UPSERT` oder erstellt die Bestellung on-the-fly). |
| **6. Einhaltung der Rate Limits (Rate Limit Buffer)** | Bei Ausfall des Webhooks startet das Status-Polling `/api/merchant/invoice/status` | Anfragen werden mit einer Pause von **mindestens 15 Sekunden** ausgeführt, was das Terminal vor der Blockierung `HTTP 429 Too Many Requests` schützt. |

### 11.1. Fertiger Satz automatischer Tests für Ihr Projekt

Bitten Sie Ihren AI-Agenten, den folgenden Testsatz zum Projekt hinzuzufügen (`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. Finale Ingenieur-Checkliste vor dem Launch

Prüfen Sie Ihr Zahlungsmodul anhand dieser 10 Punkte vor dem Umschalten auf Live-Zahlungen:

- [ ] **Website-Compliance:** Im Footer sind Links zum Öffentlichen Angebot, zur Datenschutzrichtlinie, zu den Rückgabebedingungen und den vollständigen FOP-Angaben mit Steuer-ID hinzugefügt.
- [ ] **Betrag in Kopeken:** Alle Werte `amount` sind durch `Math.round` mit 100 multipliziert ($1\text{ UAH} = 100\text{ Kopeken}$).
- [ ] **Schlüsselsicherheit:** Das Token ist in `.env` unter dem Namen `MONOBANK_TOKEN` ausgelagert und in `.gitignore` aufgenommen.
- [ ] **Preise vom Backend (SSOT):** Der Client übergibt nur die Produkt-ID, der Betrag wird ausschließlich aus der DB oder Konfiguration geholt.
- [ ] **RawBody-Verarbeitung:** Der Webhook verifiziert den originalen Text- oder Binärbuffer (`req.text()` oder `req.rawBody`), um doppeltes Parsen via `JSON.stringify` zu vermeiden.
- [ ] **Kryptografischer Schutz:** Der Webhook verifiziert die Signatur `x-sign` über den öffentlichen Schlüssel der Bank mit dem Algorithmus `SHA256` ECDSA.
- [ ] **Öffentliche URL für Webhook:** Der Server ist extern über ein gültiges SSL-Zertifikat HTTPS erreichbar (für lokale Tests eignet sich Cloudflare Tunnel oder ngrok).
- [ ] **Idempotenz:** Der erneute Eingang desselben Webhooks löst keine doppelte Ausgabe von Ware oder Abonnement aus.
- [ ] **UX der Ergebnis-Seite:** Auf `/payment-result` ist ein Wartelader und kurzes Status-Polling eingerichtet, um den Nutzer vor Race Conditions zu schützen.
- [ ] **Echte Zahlung von 1–5 UAH:** Eine erfolgreiche Testtransaktion mit echter Karte wurde durchgeführt, die Abbuchung und die Anzeige im Bankkonto wurden überprüft.

---

## Nützliche Ressourcen und Download-Dateien

- [Skills-Paket monobank-acquiring.zip herunterladen (38 KB) →](/downloads/monobank-acquiring.zip)
- [Offizielle Seite der Monobank AI Prompts →](https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts)
- [Persönliches Monobank Business-Konto →](https://web.monobank.ua/)
- [Praxisvideo auf YouTube mit vollständigem Flow-Durchlauf →](https://youtu.be/GMh_fOCiQ4E)
- [Portal zum Testen der Monobank API →](https://api.monobank.ua/)